| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
=pod |
|---|
| 32 |
|
|---|
| 33 |
=head1 NAME |
|---|
| 34 |
|
|---|
| 35 |
HoneyClient::DB - Perl extension to provide an abstract interface for |
|---|
| 36 |
storing HoneyClient data into a database. |
|---|
| 37 |
|
|---|
| 38 |
=head1 VERSION |
|---|
| 39 |
|
|---|
| 40 |
This documentation refers to HoneyClient::DB version 1.00. |
|---|
| 41 |
|
|---|
| 42 |
=head1 SYNOPSIS |
|---|
| 43 |
|
|---|
| 44 |
As a generic example, let's store data about superheroes. |
|---|
| 45 |
|
|---|
| 46 |
=head2 DEFINE SCHEMAS |
|---|
| 47 |
|
|---|
| 48 |
# First, we define a schema for each superhero ability (child object). |
|---|
| 49 |
use HoneyClient::DB; |
|---|
| 50 |
package HoneyClient::DB::SuperHero::Ability; |
|---|
| 51 |
use base("HoneyClient::DB"); |
|---|
| 52 |
|
|---|
| 53 |
# Define Ability Schema |
|---|
| 54 |
our %fields = ( |
|---|
| 55 |
string => { |
|---|
| 56 |
# Each ability should have a name. |
|---|
| 57 |
name => { |
|---|
| 58 |
# This name should be required. |
|---|
| 59 |
required => 1, # Must exist and is not null |
|---|
| 60 |
}, |
|---|
| 61 |
}, |
|---|
| 62 |
|
|---|
| 63 |
# Each ability may have an optional description. |
|---|
| 64 |
text => [ 'description' ], |
|---|
| 65 |
|
|---|
| 66 |
# Each ability may have an optional recharge time. |
|---|
| 67 |
int => [ 'recharge_time' ], |
|---|
| 68 |
); |
|---|
| 69 |
|
|---|
| 70 |
# Next, we define a schema for each superhero (parent object). |
|---|
| 71 |
package HoneyClient::DB::SuperHero; |
|---|
| 72 |
use base("HoneyClient::DB"); |
|---|
| 73 |
|
|---|
| 74 |
# Define SuperHero Schema |
|---|
| 75 |
our %fields = ( |
|---|
| 76 |
string => { |
|---|
| 77 |
# Each superhero should have a name. |
|---|
| 78 |
name => { |
|---|
| 79 |
# This name should be required. |
|---|
| 80 |
required => 1, |
|---|
| 81 |
key => $HoneyClient::DB::KEY_UNIQUE_MULT, |
|---|
| 82 |
}, |
|---|
| 83 |
# Each superhero may have an optional real name. |
|---|
| 84 |
real_name => { |
|---|
| 85 |
key => $HoneyClient::DB::KEY_UNIQUE_MULT, |
|---|
| 86 |
}, |
|---|
| 87 |
# If 2 SuperHero Objects have the same 'name' and 'real_name' |
|---|
| 88 |
# fields, then only the first object will be inserted succesfully |
|---|
| 89 |
}, |
|---|
| 90 |
|
|---|
| 91 |
# Each superhero may have optional height and weight stats. |
|---|
| 92 |
int => [ 'height', 'weight' ], |
|---|
| 93 |
|
|---|
| 94 |
# Each superhero must have a primary ability. |
|---|
| 95 |
ref => { |
|---|
| 96 |
primary_ability => { |
|---|
| 97 |
# Reference child object type. |
|---|
| 98 |
objclass=> "HoneyClient::DB::SuperHero::Ability", |
|---|
| 99 |
|
|---|
| 100 |
# This should be required. |
|---|
| 101 |
required => 1, |
|---|
| 102 |
}, |
|---|
| 103 |
}, |
|---|
| 104 |
|
|---|
| 105 |
# Each superhero may have optional abilities. |
|---|
| 106 |
array => { |
|---|
| 107 |
abilities => { |
|---|
| 108 |
# Reference child object type. |
|---|
| 109 |
objclass=> "HoneyClient::DB::SuperHero::Ability", |
|---|
| 110 |
}, |
|---|
| 111 |
}, |
|---|
| 112 |
|
|---|
| 113 |
# Each superhero should have a birth date. |
|---|
| 114 |
timestamp => { |
|---|
| 115 |
birth_date => { |
|---|
| 116 |
required => 1, |
|---|
| 117 |
}, |
|---|
| 118 |
}, |
|---|
| 119 |
); |
|---|
| 120 |
|
|---|
| 121 |
1; |
|---|
| 122 |
|
|---|
| 123 |
=head2 USE SCHEMAS |
|---|
| 124 |
|
|---|
| 125 |
# Now, we start generating data to insert into our database. |
|---|
| 126 |
|
|---|
| 127 |
use HoneyClient::DB::SuperHero; |
|---|
| 128 |
use Data::Dumper; |
|---|
| 129 |
|
|---|
| 130 |
# Create a new superhero. |
|---|
| 131 |
my $hero = { |
|---|
| 132 |
name => 'Superman', |
|---|
| 133 |
real_name => 'Clark Kent', |
|---|
| 134 |
weight => 225, |
|---|
| 135 |
height => 75, |
|---|
| 136 |
birth_date => '1998-06-01 12:34:56', # YYYY-MM-DD HH:MM:SS |
|---|
| 137 |
primary_ability => { |
|---|
| 138 |
name => 'Super Strength', |
|---|
| 139 |
description => 'More powerful than a locomotive.', |
|---|
| 140 |
}, |
|---|
| 141 |
abilities => [ |
|---|
| 142 |
{ |
|---|
| 143 |
name => 'Flight', |
|---|
| 144 |
description => "It's a bird, it's a plane.", |
|---|
| 145 |
}, |
|---|
| 146 |
{ |
|---|
| 147 |
name => 'Heat Vision', |
|---|
| 148 |
recharge_time => 5, # in seconds |
|---|
| 149 |
}, |
|---|
| 150 |
], |
|---|
| 151 |
}; |
|---|
| 152 |
|
|---|
| 153 |
# Instantiate a new SuperHero object. |
|---|
| 154 |
# Upon creation, the data will be checked against the schema. |
|---|
| 155 |
# This call will croak if any errors occur. |
|---|
| 156 |
$hero = HoneyClient::DB::SuperHero->new($hero); |
|---|
| 157 |
|
|---|
| 158 |
# Insert the data into the database. |
|---|
| 159 |
$hero->insert(); |
|---|
| 160 |
|
|---|
| 161 |
# Retrieve the superhero. |
|---|
| 162 |
my $filter = { |
|---|
| 163 |
name => 'Superman', |
|---|
| 164 |
}; |
|---|
| 165 |
|
|---|
| 166 |
# Retrieves rows in the SuperHero table where name is 'Superman'. |
|---|
| 167 |
# NOTE: At this time, the returned data is NOT identical to |
|---|
| 168 |
# the object inserted. |
|---|
| 169 |
my $inserted_hero = HoneyClient::DB::SuperHero->select($filter); |
|---|
| 170 |
|
|---|
| 171 |
# Printing the contents of the returned content should clarify |
|---|
| 172 |
# how the data looks. |
|---|
| 173 |
$Data::Dumper::Indent = 1; |
|---|
| 174 |
$Data::Dumper::Terse = 0; |
|---|
| 175 |
print Dumper($inserted_hero) . "\n"; |
|---|
| 176 |
|
|---|
| 177 |
=head1 DESCRIPTION |
|---|
| 178 |
|
|---|
| 179 |
This library is an abstract class used to access and store HoneyClient |
|---|
| 180 |
within a database. The class is not to be used directly, but can be inherited |
|---|
| 181 |
by sub-classes, in order to indirectly store specific types of data into a |
|---|
| 182 |
database. |
|---|
| 183 |
|
|---|
| 184 |
B<Note>: Any calls made to this library will fail, if a database is not properly |
|---|
| 185 |
described in the <HoneyClient/><DB/> section of the etc/honeyclient.xml |
|---|
| 186 |
configuration file or if the library cannot establish a connection to the |
|---|
| 187 |
database. |
|---|
| 188 |
|
|---|
| 189 |
=head2 SCHEMA DEFINITION |
|---|
| 190 |
|
|---|
| 191 |
The schema for a HoneyClient::DB subclass is created from the B<%fields> |
|---|
| 192 |
variable, a multi-level hash table. |
|---|
| 193 |
|
|---|
| 194 |
=head3 FIRST LEVEL: DATA TYPE |
|---|
| 195 |
|
|---|
| 196 |
The keys at the first level of B<%fields> define the data types to be used |
|---|
| 197 |
for each column, which are named as keys in the second level. The following |
|---|
| 198 |
is a list of acceptable data types: |
|---|
| 199 |
|
|---|
| 200 |
=over 4 |
|---|
| 201 |
|
|---|
| 202 |
=item * B<'int'> |
|---|
| 203 |
|
|---|
| 204 |
An integer. |
|---|
| 205 |
|
|---|
| 206 |
=item * B<'string'> |
|---|
| 207 |
|
|---|
| 208 |
A string no longer than 255 characters. |
|---|
| 209 |
|
|---|
| 210 |
=item * B<'text'> |
|---|
| 211 |
|
|---|
| 212 |
A string no longer than 65,535 characters. |
|---|
| 213 |
|
|---|
| 214 |
=item * B<'timestamp'> |
|---|
| 215 |
|
|---|
| 216 |
An ISO8601 compliant timestamp (i.e., 'MMDDYYYY HH:MM:SS'). |
|---|
| 217 |
|
|---|
| 218 |
=item * B<'array'> |
|---|
| 219 |
|
|---|
| 220 |
An array. Used to represent one-to-many relationships. |
|---|
| 221 |
|
|---|
| 222 |
B<Note>: If this type is specified, then the L<'objclass'> option |
|---|
| 223 |
must be set within each column name. |
|---|
| 224 |
|
|---|
| 225 |
=item * B<'ref'> |
|---|
| 226 |
|
|---|
| 227 |
A reference. Used to represent one-to-one relationships. |
|---|
| 228 |
|
|---|
| 229 |
B<Note>: If this type is specified, then the L<'objclass'> option |
|---|
| 230 |
must be set within each column name. |
|---|
| 231 |
|
|---|
| 232 |
=back |
|---|
| 233 |
|
|---|
| 234 |
=head3 SECOND LEVEL: COLUMN NAMES |
|---|
| 235 |
|
|---|
| 236 |
Column names are defined as keys in the second level of B<%fields>. If |
|---|
| 237 |
each column does not need any special options (e.g., making the column |
|---|
| 238 |
required), then an array reference can hold all the column names. |
|---|
| 239 |
For example, the following schema defines 3 default integer fields: |
|---|
| 240 |
|
|---|
| 241 |
%our fields = { |
|---|
| 242 |
int => [ |
|---|
| 243 |
'col_a', |
|---|
| 244 |
'col_b', |
|---|
| 245 |
'col_c', |
|---|
| 246 |
], |
|---|
| 247 |
}; |
|---|
| 248 |
|
|---|
| 249 |
However, if some of the columns need special options set (e.g., making 'col_b' |
|---|
| 250 |
required), then a sub-hash table should be defined instead, as follows: |
|---|
| 251 |
|
|---|
| 252 |
%our fields = { |
|---|
| 253 |
int => { |
|---|
| 254 |
'col_a' => {}, |
|---|
| 255 |
'col_b' => { |
|---|
| 256 |
'required' => 1, |
|---|
| 257 |
}, |
|---|
| 258 |
'col_c' => {}, |
|---|
| 259 |
}, |
|---|
| 260 |
}; |
|---|
| 261 |
|
|---|
| 262 |
=head3 THIRD LEVEL: OPTIONS |
|---|
| 263 |
|
|---|
| 264 |
If needed, options are defined in the third level of B<%fields>. These |
|---|
| 265 |
options are described as follows: |
|---|
| 266 |
|
|---|
| 267 |
=over 4 |
|---|
| 268 |
|
|---|
| 269 |
=item * B<'check_func'> |
|---|
| 270 |
|
|---|
| 271 |
If defined, then this contains a reference to a subroutine that will |
|---|
| 272 |
verify the actual column data is in a proper format. This overrides the |
|---|
| 273 |
default internal check function for the data type of that column. |
|---|
| 274 |
|
|---|
| 275 |
=item * B<'init_val'> |
|---|
| 276 |
|
|---|
| 277 |
If defined, then this value will be the default value that the database |
|---|
| 278 |
will assign to the column, if empty data is inserted into this column. |
|---|
| 279 |
|
|---|
| 280 |
=item * B<'key'> |
|---|
| 281 |
|
|---|
| 282 |
If defined, then this value creates an index for the column. |
|---|
| 283 |
Possible values are: |
|---|
| 284 |
|
|---|
| 285 |
=over 4 |
|---|
| 286 |
|
|---|
| 287 |
=item * B<$HoneyClient::DB::KEY_INDEX> |
|---|
| 288 |
|
|---|
| 289 |
If set, an index will be created in the database to improve the search |
|---|
| 290 |
time of this column. This option is only recommended for very frequently |
|---|
| 291 |
searched columns. |
|---|
| 292 |
|
|---|
| 293 |
=item * B<$HoneyClient:DB::KEY_UNIQUE> |
|---|
| 294 |
|
|---|
| 295 |
If set, a UNIQUE index is created for the column. If a record is inserted that |
|---|
| 296 |
has a match with this column in a previously existing record, the insert will |
|---|
| 297 |
fail on the database side, but the 'id' of that existing record will be |
|---|
| 298 |
returned. |
|---|
| 299 |
|
|---|
| 300 |
=item * B<$HoneyClient:DB::KEY_UNIQUE_MULT> |
|---|
| 301 |
|
|---|
| 302 |
If set, the column is added to a UNIQUE index comprised of all other columns |
|---|
| 303 |
with the KEY_UNIQUE_MULT key option. This index is used to ensure ALL VALUES for |
|---|
| 304 |
the columns in the index are distinct. An insert of a record matching an |
|---|
| 305 |
existing record will return the ID of that record. |
|---|
| 306 |
|
|---|
| 307 |
=back |
|---|
| 308 |
|
|---|
| 309 |
=item * B<'objclass'> |
|---|
| 310 |
|
|---|
| 311 |
This option is required and only used by the L<'array'> and L<'ref'> |
|---|
| 312 |
data types. The value should be a string which contains the package name of |
|---|
| 313 |
the schema to include as a child. |
|---|
| 314 |
|
|---|
| 315 |
=item * B<'required'> |
|---|
| 316 |
|
|---|
| 317 |
If defined and set to 1, then this option will cause all subsequent |
|---|
| 318 |
B<HoneyClient::DB::*-E<gt>L<new>($data)> operations to fail, if the B<$data> |
|---|
| 319 |
does not contain the required field. |
|---|
| 320 |
|
|---|
| 321 |
=back |
|---|
| 322 |
|
|---|
| 323 |
=head1 DATABASE CONFIGURATION |
|---|
| 324 |
|
|---|
| 325 |
This library expects to connect to a MySQL v5.0 or greater database. |
|---|
| 326 |
To specify which database this library should use, see the |
|---|
| 327 |
<HoneyClient/><DB/> section of the etc/honeyclient.xml configuration file |
|---|
| 328 |
for further details. |
|---|
| 329 |
|
|---|
| 330 |
=cut |
|---|
| 331 |
|
|---|
| 332 |
package HoneyClient::DB; |
|---|
| 333 |
|
|---|
| 334 |
use Data::Dumper qw(Dumper); |
|---|
| 335 |
use strict 'vars', 'subs'; |
|---|
| 336 |
use warnings; |
|---|
| 337 |
|
|---|
| 338 |
BEGIN { |
|---|
| 339 |
|
|---|
| 340 |
|
|---|
| 341 |
use DBI; |
|---|
| 342 |
use Carp (); |
|---|
| 343 |
use HoneyClient::Util::Config; |
|---|
| 344 |
use DateTime::Format::ISO8601; |
|---|
| 345 |
use Math::BigInt; |
|---|
| 346 |
use Log::Log4perl qw(:easy); |
|---|
| 347 |
|
|---|
| 348 |
require Exporter; |
|---|
| 349 |
|
|---|
| 350 |
|
|---|
| 351 |
use sigtrap qw(die untrapped normal-signals error-signals); |
|---|
| 352 |
$SIG{PIPE} = 'IGNORE'; |
|---|
| 353 |
|
|---|
| 354 |
|
|---|
| 355 |
our @ISA = qw(Exporter); |
|---|
| 356 |
our @EXPORT = qw(); |
|---|
| 357 |
our @EXPORT_OK; |
|---|
| 358 |
our $VERSION = 1.00; |
|---|
| 359 |
|
|---|
| 360 |
my $database_version; |
|---|
| 361 |
} |
|---|
| 362 |
|
|---|
| 363 |
|
|---|
| 364 |
our $LOG = get_logger(); |
|---|
| 365 |
|
|---|
| 366 |
our $dbhandle; |
|---|
| 367 |
|
|---|
| 368 |
|
|---|
| 369 |
our ( %_types, %_check, %_required, %_init_val, %_keys, %defaults ); |
|---|
| 370 |
our %display_rank; |
|---|
| 371 |
|
|---|
| 372 |
|
|---|
| 373 |
our %fields; |
|---|
| 374 |
|
|---|
| 375 |
our $last_error_code; |
|---|
| 376 |
|
|---|
| 377 |
|
|---|
| 378 |
|
|---|
| 379 |
our ( $STATUS_DELETED, $STATUS_ADDED, $STATUS_MODIFIED ) = ( 0, 1, 2 ); |
|---|
| 380 |
|
|---|
| 381 |
our ( $KEY_INDEX, $KEY_UNIQUE, $KEY_UNIQUE_MULT ) = ( 0, 1, 2 ); |
|---|
| 382 |
|
|---|
| 383 |
our ( $FIELDS_ALL, $FIELDS_SEARCH, $FIELDS_DISPLAY ) = ( 0 , 1, 2); |
|---|
| 384 |
|
|---|
| 385 |
our ($ERROR_NONE,$ERROR_INSERT_FAILED,$ERROR_DUPLICATE_FOUND,$ERROR_DUPLICATE_UNRESOLVED) |
|---|
| 386 |
= (0,1,2,3); |
|---|
| 387 |
our @ERROR_MESSAGES = ( |
|---|
| 388 |
"Success", |
|---|
| 389 |
"Failed with a fatal error", |
|---|
| 390 |
"Duplicate object found. Non-fatal warning", |
|---|
| 391 |
"Duplicate object found. Unable to retrieve ID of duplicate record", |
|---|
| 392 |
); |
|---|
| 393 |
our $LAST_ERROR = $ERROR_NONE; |
|---|
| 394 |
|
|---|
| 395 |
|
|---|
| 396 |
our %config; |
|---|
| 397 |
$config{driver} = "mysql"; |
|---|
| 398 |
$config{host} = getVar( name => "host", namespace => "HoneyClient::DB" ); |
|---|
| 399 |
$config{port} = getVar( name => "port", namespace => "HoneyClient::DB" ); |
|---|
| 400 |
$config{user} = getVar( name => "user", namespace => "HoneyClient::DB" ); |
|---|
| 401 |
$config{pass} = getVar( name => "pass", namespace => "HoneyClient::DB" ); |
|---|
| 402 |
$config{dbname} = getVar( name => "dbname", namespace => "HoneyClient::DB" ); |
|---|
| 403 |
|
|---|
| 404 |
if ( !db_exists(%config) ) { |
|---|
| 405 |
die; |
|---|
| 406 |
} |
|---|
| 407 |
|
|---|
| 408 |
END { |
|---|
| 409 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 410 |
} |
|---|
| 411 |
|
|---|
| 412 |
=pod |
|---|
| 413 |
|
|---|
| 414 |
=head1 METHODS |
|---|
| 415 |
|
|---|
| 416 |
=head2 Object Creation |
|---|
| 417 |
|
|---|
| 418 |
=over 4 |
|---|
| 419 |
|
|---|
| 420 |
=item new |
|---|
| 421 |
|
|---|
| 422 |
Receives an unblessed hash, imports the schema (if necessary), checks that |
|---|
| 423 |
required fields contain the proper data, and returns the blessed object. |
|---|
| 424 |
|
|---|
| 425 |
It must be called using an object class derived from HoneyClient::DB. |
|---|
| 426 |
For Example: |
|---|
| 427 |
|
|---|
| 428 |
$my_obj = HoneyClient::DB::SomeObj->new({ |
|---|
| 429 |
field_a => "foo", |
|---|
| 430 |
field_b => "bar" |
|---|
| 431 |
}); |
|---|
| 432 |
|
|---|
| 433 |
=cut |
|---|
| 434 |
|
|---|
| 435 |
sub new { |
|---|
| 436 |
my ( $class, $self ) = @_; |
|---|
| 437 |
bless( $self, $class ); |
|---|
| 438 |
|
|---|
| 439 |
|
|---|
| 440 |
import_schema($class) if ( !exists( $_types{$class} ) ); |
|---|
| 441 |
|
|---|
| 442 |
|
|---|
| 443 |
my @missing = $self->_check_required(); |
|---|
| 444 |
if ( scalar @missing ) { |
|---|
| 445 |
$LOG->fatal( "$class->new(): Object missing required attribute(s): " . |
|---|
| 446 |
join( ', ', @missing ) . '.' ); |
|---|
| 447 |
Carp::croak( "$class->new(): Object missing required attribute(s): " . |
|---|
| 448 |
join( ', ', @missing ) . '.\n' ); |
|---|
| 449 |
} |
|---|
| 450 |
|
|---|
| 451 |
|
|---|
| 452 |
foreach my $key ( keys %$self ) { |
|---|
| 453 |
eval { |
|---|
| 454 |
if ( $self->{$key} ) |
|---|
| 455 |
{ |
|---|
| 456 |
$self->{$key} = $_check{$class}{$key}->( $self->{$key} ); |
|---|
| 457 |
} |
|---|
| 458 |
}; |
|---|
| 459 |
if ($@) { |
|---|
| 460 |
$LOG->fatal("Invalid Object $key\t$@"); |
|---|
| 461 |
Carp::croak "Invalid Object $key\n\t$@"; |
|---|
| 462 |
} |
|---|
| 463 |
if ( $_types{$class}{$key} =~ m/(array|ref):(.*)/ ) { |
|---|
| 464 |
my $ref = ref( $self->{$key} ); |
|---|
| 465 |
my ($childType,$childClass) = ($1,$2); |
|---|
| 466 |
|
|---|
| 467 |
if ( $childClass->can('new') ) { |
|---|
| 468 |
if ( $childType eq 'ref' ) { |
|---|
| 469 |
$self->{$key} = $childClass->new( $self->{$key} ); |
|---|
| 470 |
} |
|---|
| 471 |
if ( $ref eq 'ARRAY' and $childType eq 'array' ) { |
|---|
| 472 |
foreach my $obj ( @{ $self->{$key} } ) { |
|---|
| 473 |
$obj = $childClass->new($obj); |
|---|
| 474 |
} |
|---|
| 475 |
} |
|---|
| 476 |
} |
|---|
| 477 |
else { |
|---|
| 478 |
$LOG->fatal("Invalid Object! $childType does not exist"); |
|---|
| 479 |
Carp::croak "Invalid Object! $childType does not exist"; |
|---|
| 480 |
} |
|---|
| 481 |
} |
|---|
| 482 |
} |
|---|
| 483 |
return $self; |
|---|
| 484 |
} |
|---|
| 485 |
|
|---|
| 486 |
|
|---|
| 487 |
|
|---|
| 488 |
sub _check_required { |
|---|
| 489 |
my $self = shift; |
|---|
| 490 |
my $class = ref $self; |
|---|
| 491 |
|
|---|
| 492 |
|
|---|
| 493 |
if ( exists $_required{$class} ) { |
|---|
| 494 |
my @missing; |
|---|
| 495 |
foreach ( keys %{ $_required{$class} } ) { |
|---|
| 496 |
push( @missing, $_ ) |
|---|
| 497 |
if ( !defined( $self->{$_} ) or ( $self->{$_} eq "" ) ); |
|---|
| 498 |
} |
|---|
| 499 |
return @missing; |
|---|
| 500 |
} |
|---|
| 501 |
return; |
|---|
| 502 |
} |
|---|
| 503 |
|
|---|
| 504 |
sub import_schema { |
|---|
| 505 |
my $class = shift; |
|---|
| 506 |
my $schema = \%{ $class . "::fields" }; |
|---|
| 507 |
my (%rank_display, %rank_search); |
|---|
| 508 |
my $MAX_RANK = 10000; |
|---|
| 509 |
|
|---|
| 510 |
|
|---|
| 511 |
while ( my ( $type, $attrib ) = each(%$schema) ) { |
|---|
| 512 |
my $ref = ref $attrib; |
|---|
| 513 |
|
|---|
| 514 |
|
|---|
| 515 |
if ( $ref eq 'ARRAY' ) { |
|---|
| 516 |
foreach ( @{$attrib} ) { |
|---|
| 517 |
$_types{$class}{$_} = $type; |
|---|
| 518 |
if ( $type =~ m/(ref|array)/ ) { |
|---|
| 519 |
delete $_types{$class}; |
|---|
| 520 |
$LOG->fatal("Invalid Object Type. ref AND array types must " |
|---|
| 521 |
. "be defined as a hash containing 'objclass'"); |
|---|
| 522 |
Carp::croak "Invalid Object Type. ref AND array types must " |
|---|
| 523 |
. "be defined as a hash containing 'objclass'"; |
|---|
| 524 |
} |
|---|
| 525 |
$_check{$class}{$_} = $defaults{$type}{check_func} |
|---|
| 526 |
or $_check{$class}{$_} = \&check_nothing; |
|---|
| 527 |
} |
|---|
| 528 |
} |
|---|
| 529 |
|
|---|
| 530 |
|
|---|
| 531 |
elsif ( $ref eq 'HASH' ) { |
|---|
| 532 |
while ( my ( $a, $opts ) = each %$attrib ) { |
|---|
| 533 |
$_types{$class}{$a} = $type; |
|---|
| 534 |
if ( exists $opts->{required} && $opts->{required}) { |
|---|
| 535 |
$_required{$class}{$a} = 1; |
|---|
| 536 |
} |
|---|
| 537 |
|
|---|
| 538 |
|
|---|
| 539 |
if ( $type =~ m/^(array|ref)$/ ) { |
|---|
| 540 |
if ( !exists $opts->{objclass} ) { |
|---|
| 541 |
$LOG->fatal("$1 of unknown class: $a"); |
|---|
| 542 |
Carp::croak "$1 of unknown class: $a"; |
|---|
| 543 |
} |
|---|
| 544 |
if ( !exists $_types{ $opts->{objclass} } ) { |
|---|
| 545 |
import_schema( $opts->{objclass} ); |
|---|
| 546 |
} |
|---|
| 547 |
$_types{$class}{$a} .= ':' . $opts->{objclass}; |
|---|
| 548 |
} |
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
if ( $opts->{check_func} ) { |
|---|
| 552 |
$_check{$class}{$a} = $opts->{check_func}; |
|---|
| 553 |
} |
|---|
| 554 |
else { |
|---|
| 555 |
$_check{$class}{$a} = $defaults{$type}{check_func} |
|---|
| 556 |
or $_check{$class}{$a} = \&check_nothing; |
|---|
| 557 |
} |
|---|
| 558 |
|
|---|
| 559 |
|
|---|
| 560 |
if ( $opts->{key} ) { |
|---|
| 561 |
$_keys{$class}{$a} = $opts->{key}; |
|---|
| 562 |
} |
|---|
| 563 |
if ( $opts->{init_val} ) { |
|---|
| 564 |
$_init_val{$class}{$a} = $opts->{key}; |
|---|
| 565 |
} |
|---|
| 566 |
|
|---|
| 567 |
if( $opts->{searchable} ) { |
|---|
| 568 |
|
|---|
| 569 |
} |
|---|
| 570 |
if( exists $opts->{display_rank} ) { |
|---|
| 571 |
my $rank = $opts->{display_rank}; |
|---|
| 572 |
if ($opts->{display_rank}) { |
|---|
| 573 |
while (exists $rank_display{$rank}) { |
|---|
| 574 |
$rank++; |
|---|
| 575 |
} |
|---|
| 576 |
$rank_display{$rank} = $a; |
|---|
| 577 |
} |
|---|
| 578 |
} |
|---|
| 579 |
elsif ($type =~ m/^(array|ref)$/) {} |
|---|
| 580 |
else { |
|---|
| 581 |
$rank_display{$MAX_RANK++} = $a; |
|---|
| 582 |
} |
|---|
| 583 |
} |
|---|
| 584 |
} |
|---|
| 585 |
else { |
|---|
| 586 |
$LOG->warn("$class\{$type\} is defined improperly"); |
|---|
| 587 |
} |
|---|
| 588 |
} |
|---|
| 589 |
my @temp = map $rank_display{$_}, sort( keys( %rank_display ) ); |
|---|
| 590 |
$display_rank{$class} = \@temp; |
|---|
| 591 |
|
|---|
| 592 |
|
|---|
| 593 |
if (!$class->deploy_table()) { |
|---|
| 594 |
$LOG->fatal("${class}->import_schema: " . "Failed to deploy table"); |
|---|
| 595 |
Carp::croak("${class}->import_schema: " . "Failed to deploy table"); |
|---|
| 596 |
} |
|---|
| 597 |
} |
|---|
| 598 |
|
|---|
| 599 |
=back |
|---|
| 600 |
|
|---|
| 601 |
=head2 Database Operations |
|---|
| 602 |
|
|---|
| 603 |
=over 4 |
|---|
| 604 |
|
|---|
| 605 |
=item insert |
|---|
| 606 |
|
|---|
| 607 |
Creates and executes a SQL INSERT statement for the referenced object. The |
|---|
| 608 |
object must be initialized at the time this method is called. |
|---|
| 609 |
|
|---|
| 610 |
$my_obj->insert(); |
|---|
| 611 |
|
|---|
| 612 |
B<Input> |
|---|
| 613 |
|
|---|
| 614 |
There are no parameters, however the calling object is used as input for the |
|---|
| 615 |
insert operation. |
|---|
| 616 |
|
|---|
| 617 |
B<Return Value> |
|---|
| 618 |
|
|---|
| 619 |
Returns the 'id' of the (parent) object inserted. |
|---|
| 620 |
|
|---|
| 621 |
=cut |
|---|
| 622 |
|
|---|
| 623 |
sub insert { |
|---|
| 624 |
my $obj = shift; |
|---|
| 625 |
my $id = undef; |
|---|
| 626 |
my $objType = ref $obj; |
|---|
| 627 |
|
|---|
| 628 |
$dbhandle = HoneyClient::DB::_connect(%config); |
|---|
| 629 |
|
|---|
| 630 |
|
|---|
| 631 |
$LOG->debug("Attempting insert operation."); |
|---|
| 632 |
eval { $id = _insert( $obj, undef ); }; |
|---|
| 633 |
if ($@) { |
|---|
| 634 |
$LOG->warn("insert failed, Rolling Back: $@"); |
|---|
| 635 |
$dbhandle->rollback(); |
|---|
| 636 |
} |
|---|
| 637 |
elsif ($LAST_ERROR != $ERROR_NONE) { |
|---|
| 638 |
$LOG->warn("Rolling Back $objType Insert. Code #$LAST_ERROR: ".$ERROR_MESSAGES[$LAST_ERROR]); |
|---|
| 639 |
$dbhandle->rollback(); |
|---|
| 640 |
} |
|---|
| 641 |
else { |
|---|
| 642 |
$dbhandle->commit(); |
|---|
| 643 |
} |
|---|
| 644 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 645 |
return $id; |
|---|
| 646 |
} |
|---|
| 647 |
|
|---|
| 648 |
|
|---|
| 649 |
|
|---|
| 650 |
sub _insert { |
|---|
| 651 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 652 |
my $ref = ref $obj; |
|---|
| 653 |
|
|---|
| 654 |
if ( $ref eq 'ARRAY' ) { |
|---|
| 655 |
return _insert_array( $obj, $fk_col, $fk_id ); |
|---|
| 656 |
} |
|---|
| 657 |
elsif ( exists $_types{$ref} ) { |
|---|
| 658 |
return _insert_obj( $obj, $fk_col, $fk_id ); |
|---|
| 659 |
} |
|---|
| 660 |
elsif ($ref) { |
|---|
| 661 |
$LOG->warn("Can't insert object of type $ref"); |
|---|
| 662 |
} |
|---|
| 663 |
else { |
|---|
| 664 |
$LOG->warn("Attempted to insert scalar value into the database"); |
|---|
| 665 |
} |
|---|
| 666 |
return undef; |
|---|
| 667 |
} |
|---|
| 668 |
|
|---|
| 669 |
sub _insert_array { |
|---|
| 670 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 671 |
my @entries; |
|---|
| 672 |
foreach (@$obj) { |
|---|
| 673 |
my $id = _insert( $_, $fk_col, $fk_id ); |
|---|
| 674 |
ref($id) eq 'ARRAY' ? push( @entries, @$id ) : push( @entries, $id ); |
|---|
| 675 |
} |
|---|
| 676 |
return \@entries; |
|---|
| 677 |
} |
|---|
| 678 |
|
|---|
| 679 |
sub _insert_obj { |
|---|
| 680 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 681 |
my ( $class, $table ) = ( ref($obj), _get_table($obj) ); |
|---|
| 682 |
my ( $id, %insert, %index, %children ); |
|---|
| 683 |
my $error = $ERROR_NONE; |
|---|
| 684 |
|
|---|
| 685 |
|
|---|
| 686 |
while ( my ( $col, $data ) = each %$obj ) { |
|---|
| 687 |
if ( !$_types{$class}{$col} ) { |
|---|
| 688 |
$LOG->warn("$col=>$data is not a valid field in $class"); |
|---|
| 689 |
delete $obj->{$col}; |
|---|
| 690 |
} |
|---|
| 691 |
|
|---|
| 692 |
elsif ( $_types{$class}{$col} =~ m/(array)/ ) { |
|---|
| 693 |
$children{$col} = $data; |
|---|
| 694 |
} |
|---|
| 695 |
|
|---|
| 696 |
elsif ( $_types{$class}{$col} =~ m/ref:(.*)/ ) { |
|---|
| 697 |
if ( my $ft = $1->_get_table() ) { |
|---|
| 698 |
$insert{ $ft . '_' .$col . '_fk' } = _insert($data); |
|---|
| 699 |
} |
|---|
| 700 |
} |
|---|
| 701 |
|
|---|
| 702 |
else { |
|---|
| 703 |
$insert{$col} = $dbhandle->quote($data); |
|---|
| 704 |
} |
|---|
| 705 |
} |
|---|
| 706 |
|
|---|
| 707 |
$insert{$fk_col} = $fk_id if ( $fk_col && $fk_id ); |
|---|
| 708 |
|
|---|
| 709 |
|
|---|
| 710 |
my $sql = "INSERT INTO $table (" . join( ',', keys %insert ) . ") VALUES (" . join( ',', values(%insert) ) . ')'; |
|---|
| 711 |
eval { |
|---|
| 712 |
$LOG->debug($sql); |
|---|
| 713 |
$dbhandle->do($sql); |
|---|
| 714 |
}; |
|---|
| 715 |
|
|---|
| 716 |
if ($@) { |
|---|
| 717 |
if ( $dbhandle->err == 1062 ) { |
|---|
| 718 |
my $filter; |
|---|
| 719 |
while ( my ( $col, $key_type ) = each %{ $_keys{$class} } ) { |
|---|
| 720 |
if ( $key_type == $KEY_UNIQUE || $key_type == $KEY_UNIQUE_MULT ) |
|---|
| 721 |
{ |
|---|
| 722 |
$filter->{$col} = $obj->{$col}; |
|---|
| 723 |
} |
|---|
| 724 |
} |
|---|
| 725 |
|
|---|
| 726 |
my @rows = $class->_select( |
|---|
| 727 |
'-columns'=>['id'], |
|---|
| 728 |
'-where'=>$filter, |
|---|
| 729 |
); |
|---|
| 730 |
if (scalar @rows) { |
|---|
| 731 |
$id = $rows[0]->{id}; |
|---|
| 732 |
$error = $ERROR_DUPLICATE_FOUND; |
|---|
| 733 |
} |
|---|
| 734 |
else { |
|---|
| 735 |
$LAST_ERROR = $ERROR_DUPLICATE_UNRESOLVED; |
|---|
| 736 |
$LOG->fatal("Error: Can't resolve duplicate records\t" . $dbhandle->err . ": $@"); |
|---|
| 737 |
Carp::croak("Error: Can't resolve duplicate records\n\t" . $dbhandle->err . ": $@"); |
|---|
| 738 |
} |
|---|
| 739 |
} |
|---|
| 740 |
else { |
|---|
| 741 |
$LAST_ERROR = $ERROR_INSERT_FAILED; |
|---|
| 742 |
$LOG->fatal("Error #".$dbhandle->err."while executing SQL:\n\t$sql\n$@"); |
|---|
| 743 |
Carp::croak("Error #".$dbhandle->err."while executing SQL:\n\t$sql\n$@"); |
|---|
| 744 |
} |
|---|
| 745 |
} |
|---|
| 746 |
else { |
|---|
| 747 |
$id = $dbhandle->{'mysql_insertid'}; |
|---|
| 748 |
} |
|---|
| 749 |
|
|---|
| 750 |
foreach ( keys %children ) { |
|---|
| 751 |
my $rv = _insert( $children{$_}, $table . '_' .$_ . '_fk', $id ); |
|---|
| 752 |
} |
|---|
| 753 |
$LAST_ERROR = $error; |
|---|
| 754 |
return $id; |
|---|
| 755 |
} |
|---|
| 756 |
|
|---|
| 757 |
=head3 QUERY CONDITIONS |
|---|
| 758 |
|
|---|
| 759 |
The following functions accept conditions using the '-where' argument. These |
|---|
| 760 |
conditions filter results to the desired information. Currently any scalar field |
|---|
| 761 |
is searchable (string, int, text, timestamp), while ref objects are searchable |
|---|
| 762 |
only by their ids (for now). |
|---|
| 763 |
|
|---|
| 764 |
The following query will list Regkey changes that occurred to the Windows |
|---|
| 765 |
auto-start key . |
|---|
| 766 |
|
|---|
| 767 |
@runKeys = HoneyClient::DB::Regkey->select( |
|---|
| 768 |
-where => { |
|---|
| 769 |
key_name => 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', |
|---|
| 770 |
}, |
|---|
| 771 |
); |
|---|
| 772 |
|
|---|
| 773 |
In order to perform a partial match search, a tilda can be added to the front |
|---|
| 774 |
of the column name. This is not applicable to ranges. The following query will |
|---|
| 775 |
return any Registry change with the string 'Run' in the key name(path). |
|---|
| 776 |
|
|---|
| 777 |
@runKeys = HoneyClient::DB::Regkey->select( |
|---|
| 778 |
-where => { |
|---|
| 779 |
'~key_name' => 'Run', |
|---|
| 780 |
}, |
|---|
| 781 |
); |
|---|
| 782 |
|
|---|
| 783 |
B<NOTE:> In this case, the column name must be quoted, or used as a variable. |
|---|
| 784 |
|
|---|
| 785 |
A query can search a range, for instance, the following will show all |
|---|
| 786 |
Honeyclients in a given date range. |
|---|
| 787 |
|
|---|
| 788 |
@octoberClients = HoneyClient::DB::Client->select( |
|---|
| 789 |
-where => { |
|---|
| 790 |
start_time => ['2007-10-01 00::00::00','2007-10-31 23::59::59' |
|---|
| 791 |
}, |
|---|
| 792 |
); |
|---|
| 793 |
|
|---|
| 794 |
The '-where' option accepts filters on multiple columns as well. |
|---|
| 795 |
|
|---|
| 796 |
|
|---|
| 797 |
=item select |
|---|
| 798 |
|
|---|
| 799 |
Creates and executes a SQL SELECT statement and returns an array of hash refs |
|---|
| 800 |
containing result rows. |
|---|
| 801 |
|
|---|
| 802 |
The following would select the name, age, address, and phone columns for all |
|---|
| 803 |
records with the name 'bob', and an age in the range of 25-50 (inclusive). |
|---|
| 804 |
|
|---|
| 805 |
@my_objects = HoneyClient::DB::Process->select( |
|---|
| 806 |
-columns => qw(name,pid,parent_name,created_time), |
|---|
| 807 |
-where => { |
|---|
| 808 |
name => 'bob', |
|---|
| 809 |
age => [25,50], |
|---|
| 810 |
}, |
|---|
| 811 |
); |
|---|
| 812 |
|
|---|
| 813 |
B<Input> |
|---|
| 814 |
|
|---|
| 815 |
The select() function can take as arguments a hash containing the following keys: |
|---|
| 816 |
|
|---|
| 817 |
=item * B<'-columns'> |
|---|
| 818 |
|
|---|
| 819 |
List of columns to be selected. By default, if no -columns argument is given, |
|---|
| 820 |
all columns are selected. See L<get_fields>. |
|---|
| 821 |
|
|---|
| 822 |
=item * B<'-where'> |
|---|
| 823 |
|
|---|
| 824 |
This field can contain a set of conditions used to filter the results. Refer to |
|---|
| 825 |
L<QUERY CONDITIONS> for more info. |
|---|
| 826 |
|
|---|
| 827 |
=cut |
|---|
| 828 |
|
|---|
| 829 |
sub select { |
|---|
| 830 |
|
|---|
| 831 |
$dbhandle = HoneyClient::DB::_connect(%config); |
|---|
| 832 |
my @results = _select(@_); |
|---|
| 833 |
|
|---|
| 834 |
|
|---|
| 835 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 836 |
wantarray ? return @results : return \@results; |
|---|
| 837 |
} |
|---|
| 838 |
|
|---|
| 839 |
sub _select { |
|---|
| 840 |
my (@results,$sql); |
|---|
| 841 |
eval { |
|---|
| 842 |
|
|---|
| 843 |
$sql = _build_query(@_); |
|---|
| 844 |
$LOG->debug($sql); |
|---|
| 845 |
|
|---|
| 846 |
|
|---|
| 847 |
my $sth = $dbhandle->prepare($sql); |
|---|
| 848 |
$sth->execute(); |
|---|
| 849 |
|
|---|
| 850 |
|
|---|
| 851 |
while ( my $row = $sth->fetchrow_hashref() ) { |
|---|
| 852 |
push @results, $row; |
|---|
| 853 |
} |
|---|
| 854 |
}; |
|---|
| 855 |
if ($@) { |
|---|
| 856 |
$LOG->warn("Error while executing SQL:\n\t$sql\n$@"); |
|---|
| 857 |
return (); |
|---|
| 858 |
} |
|---|
| 859 |
return @results; |
|---|
| 860 |
} |
|---|
| 861 |
|
|---|
| 862 |
sub _build_query { |
|---|
| 863 |
my ( $class, %args) = @_; |
|---|
| 864 |
my @fields; |
|---|
| 865 |
if (exists($args{'-columns'}) && scalar(@{$args{'-columns'}})) { |
|---|
| 866 |
@fields = @{$args{'-columns'}}; |
|---|
| 867 |
} |
|---|
| 868 |
else { |
|---|
| 869 |
@fields = $class->get_fields(); |
|---|
| 870 |
} |
|---|
| 871 |
|
|---|
| 872 |
foreach my $col_def (@fields) { |
|---|
| 873 |
if (ref $col_def eq 'HASH') { |
|---|
| 874 |
while (my ($col,$as) = (each %$col_def)) { |
|---|
| 875 |
push @fields, "$col AS '$as'"; |
|---|
| 876 |
} |
|---|
| 877 |
} |
|---|
| 878 |
} |
|---|
| 879 |
|
|---|
| 880 |
my $sql = "SELECT "; $sql .= join( ',', @fields); $sql .= " FROM " . $class->_get_table(); |
|---|
| 881 |
|
|---|
| 882 |
$sql .= $class->_where_condition($args{'-where'}); |
|---|
| 883 |
return $sql; |
|---|
| 884 |
} |
|---|
| 885 |
|
|---|
| 886 |
|
|---|
| 887 |
sub _where_condition { |
|---|
| 888 |
my ($class,$cond) = @_; |
|---|
| 889 |
my @conditions; |
|---|
| 890 |
if (!keys(%$cond)) { |
|---|
| 891 |
return ''; |
|---|
| 892 |
} |
|---|
| 893 |
while ( my ( $col, $data ) = each %$cond ) { |
|---|
| 894 |
my $partial = 0; |
|---|
| 895 |
if ($col =~ /^\~(.*)/) { |
|---|
| 896 |
$col = $1; |
|---|
| 897 |
$partial = 1; |
|---|
| 898 |
} |
|---|
| 899 |
if ( !exists $_types{$class}{$col} && $col ne 'id' && $col !~ m/_fk$/) { |
|---|
| 900 |
next; |
|---|
| 901 |
} |
|---|
| 902 |
my $cf = ($_check{$class}{$col} || \&check_nothing); |
|---|
| 903 |
if (my $ref = ref($data)) { |
|---|
| 904 |
eval { |
|---|
| 905 |
if (my ($type,$sub_class) = $_types{$class}{$col} =~ /(array|ref):(.*)/ ) { |
|---|
| 906 |
if ($ref eq 'HASH') { |
|---|
| 907 |
my ($left,$right) = ('id',$class->get_col_name($col)); |
|---|
| 908 |
if ($type eq 'ref') { |
|---|
| 909 |
($left, $right) = ($right,$left); |
|---|
| 910 |
} |
|---|
| 911 |
my $sub_query = $sub_class->select( |
|---|
| 912 |
-columns => [$right], |
|---|
| 913 |
-where => $data, |
|---|
| 914 |
); |
|---|
| 915 |
push @conditions, $left.' IN ('.$sub_query.')'; |
|---|
| 916 |
} |
|---|
| 917 |
} |
|---|
| 918 |
elsif ($ref eq 'ARRAY' && &$cf($data->[0]) && &$cf($data->[1])) { |
|---|
| 919 |
push @conditions, ( $class->get_col_name($col) . ' BETWEEN ' . |
|---|
| 920 |
$dbhandle->quote($data->[0]) . ' AND '. |
|---|
| 921 |
$dbhandle->quote($data->[1]) |
|---|
| 922 |
); |
|---|
| 923 |
} |
|---|
| 924 |
}; |
|---|
| 925 |
|
|---|
| 926 |
} |
|---|
| 927 |
else { |
|---|
| 928 |
if (exists $_types{$class}{$col} && $_types{$class}{$col} =~ /ref:(.*)/ ) { |
|---|
| 929 |
if (check_int($data)) { |
|---|
| 930 |
push @conditions,($class->get_col_name($col). |
|---|