ccooper%deadsquid.com 2c97411785 b=445248
- don't memoize anything


git-svn-id: svn://10.0.0.236/trunk@255521 18797224-902f-48f8-a5cc-f745e15eee43
2008-12-15 16:37:28 +00:00

399 lines
11 KiB
Perl
Executable File

# -*- mode: cperl; c-basic-offset: 8; indent-tabs-mode: nil; -*-
=head1 COPYRIGHT
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License
# at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
# the License for the specific language governing rights and
# limitations under the License.
#
# The Original Code is Litmus.
#
# The Initial Developer of the Original Code is
# the Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2006
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Chris Cooper <ccooper@deadsquid.com>
# Zach Lipton <zach@zachlipton.com>
#
# ***** END LICENSE BLOCK *****
=cut
#########################################################################
# We're overiding at the base level so we can subclass some functions to grab
# auditing information automatically.
package AuditDBI;
use strict;
use base 'DBIx::ContextualFetch';
#########################################################################
package AuditDBI::db;
use base 'DBIx::ContextualFetch::db';
use Litmus::Config;
sub do {
my ($dbh) = shift;
my ($sql) = shift;
my ($attr) = shift;
my @bind_values = @_;
my $rv = $dbh->SUPER::do($sql,$attr,@bind_values);
if ($rv and $Litmus::Config::AUDIT_TRAIL) {
my $audit_rv = $dbh->_audit_action($sql,@bind_values);
}
return $rv;
}
sub _audit_action {
my ($dbh) = shift;
my ($sql) = shift;
my @bind_values = @_;
my ($action_type) = ($sql =~ /^(INSERT|UPDATE|DELETE)/i);
$action_type = uc($action_type);
if (&_ignore_this_action($action_type,$sql)) {
return 1;
}
my $user = Litmus::Auth::getCurrentUser();
if (!$user or
!$user->isInAdminGroup()) {
return 1;
}
my $bind_values_string = &_bind_values_to_string(@bind_values);
my $audit_sql = "INSERT INTO audit_trail (user_id,action_timestamp,action_type,sql_log,bind_values) VALUES (?,NOW(),?,?,?)";
my $rv = $dbh->SUPER::do($audit_sql,
undef,
$user->{'user_id'},
$action_type,
$sql,
$bind_values_string
);
return $rv;
}
sub _bind_values_to_string {
my $bind_values_string = "";
foreach my $bind_value (@_) {
if ($bind_values_string ne "") {
$bind_values_string .= ",";
}
next if (!$bind_value);
if ($bind_value =~ /^\d+$/) {
$bind_values_string .= $bind_value;
} else {
$bind_values_string .= "'" . $bind_value . "'";
}
}
return $bind_values_string;
}
sub _ignore_this_action {
my ($action_type,$sql) = @_;
if (%Litmus::Config::AUDIT_ACTIONS_TO_IGNORE) {
if ($Litmus::Config::AUDIT_ACTIONS_TO_IGNORE{$action_type} and
scalar $Litmus::Config::AUDIT_ACTIONS_TO_IGNORE{$action_type} > 0) {
foreach my $table_name (@{$Litmus::Config::AUDIT_ACTIONS_TO_IGNORE{$action_type}}) {
if ($sql =~ /^$action_type\s+(INTO|FROM|)\s*$table_name/i) {
return 1;
}
}
}
}
return 0;
}
#########################################################################
package AuditDBI::st;
use base 'DBIx::ContextualFetch::st';
use Litmus::Config;
sub execute {
my ($sth) = shift;
my @bind_values = @_;
if ($sth->{Statement} =~ /^(INSERT|UPDATE|DELETE)/i) {
my $rv = $sth->SUPER::execute(@bind_values);
if ($rv and $Litmus::Config::AUDIT_TRAIL) {
my $dbh = $sth->{Database};
my $audit_rv = $dbh->_audit_action($sth->{Statement},@bind_values);
}
return $rv;
}
return $sth->SUPER::execute(@bind_values);
}
#########################################################################
package Litmus::DBI;
require Apache::DBI;
use strict;
use warnings;
use Class::DBI;
use DBI;
use Encode qw( encode_utf8 decode_utf8 );
use Litmus::Config;
use utf8;
use base qw( Exporter Class::Data::Inheritable Class::DBI::mysql );
use Class::DBI::Plugin::RetrieveAll;
use constant MP2 => ( exists $ENV{MOD_PERL_API_VERSION} and
$ENV{MOD_PERL_API_VERSION} >= 2 );
use constant MP1 => ( exists $ENV{MOD_PERL} and
! exists $ENV{MOD_PERL_API_VERSION});
# export the following functions..
our @EXPORT = (qw( utf8_all_columns utf8_columns ));
# Our handles
our $_dbh;
our $_ro_dbh;
our %column_aliases;
Litmus::DBI->autoupdate(0);
# add an accessor to store which columns are utf8-enabled
Class::DBI->mk_classdata('_utf8_columns');
__PACKAGE__->db_Main();
__PACKAGE__->db_ReadOnly();
# In some cases, we have column names that make sense from a database perspective
# (i.e. subgroup_id), but that don't make sense from a class/object perspective
# (where subgroup would be more appropriate). To handle this, we allow for
# Litmus::DBI's subclasses to set column aliases with the column_alias() sub.
# Takes the database column name and the alias name.
sub column_alias {
my ($self, $db_name, $alias_name) = @_;
$column_aliases{$alias_name} = $db_name;
}
# here's where the actual work happens. We consult our alias list
# (as created by calls to column_alias()) and substitute the
# database column if we find a match
sub find_column {
my $self = shift;
my $wanted = shift;
if (ref $self) {
$wanted =~ s/^.*::(\w+)$/$1/;
}
if ($column_aliases{$wanted}) {
return $column_aliases{$wanted};
} else {
# not an alias, so we use the normal
# find_column() from Class::DBI
$self->SUPER::find_column($wanted);
}
}
sub AUTOLOAD {
my $self = shift;
my @args = @_;
my $name = our $AUTOLOAD;
my $col = $self->find_column($name);
if (!$col) {
lastDitchError("tried to call Litmus::DBI method $name which does not exist");
}
return $self->$col(@args);
}
sub _log {
my ($self, $message, %info) = @_;
binmode(STDERR,':utf8');
Litmus::Error::logError($message, caller(0));
return;
}
# DBI error handler for SQL errors:
sub _croak {
my ($self, $message, %info) = @_;
lastDitchError($message);
return;
}
sub lastDitchError($) {
my $message = shift;
print "Error - Litmus has suffered a serious fatal internal error - $message";
exit;
}
# hack around a bug where auto_increment columns don't work properly unless
# the auto_increment key is explicitly set to null in insert statements:
sub _auto_increment_value {
my $self = shift;
my $dbh = $self->db_Main;
my $id;
eval {
my $sth = $dbh->prepare("SELECT LAST_INSERT_ID()");
$sth->execute();
my @data = $sth->fetchrow_array();
$id = $data[0];
} or return $self->SUPER::_auto_increment_value();
if (! defined $id) { return $self->SUPER::_auto_increment_value() }
return $id;
}
__PACKAGE__->_remember_handle('Main'); # so dbi_commit works
# override default to avoid using Ima::DBI closure
sub db_Main {
my $dsn = "dbi:mysql(RootClass=AuditDBI):database=$Litmus::Config::db_name;host=$Litmus::Config::db_host;port=$Litmus::Config::db_port";
if ( !$_dbh or !$_dbh->ping()) {
# $config is my config object. replace with your own settings...
$_dbh = DBI->connect_cached($dsn,
$Litmus::Config::db_user,
$Litmus::Config::db_pass,
{ mysql_enable_utf8 => 1,
wait_timeout => 60*60*8,
mysql_auto_reconnect => 1 }
);
}
return $_dbh;
}
sub db_ReadOnly() {
my $dsn = "dbi:mysql(RootClass=AuditDBI):database=$Litmus::Config::db_name;host=$Litmus::Config::db_host;port=$Litmus::Config::db_port";
if (defined $Litmus::Config::db_host_ro) {
if ( !$_ro_dbh or !$_ro_dbh->ping()) {
# $config is my config object. replace with your own settings...
$_ro_dbh = DBI->connect_cached($dsn,
$Litmus::Config::db_user_ro,
$Litmus::Config::db_pass_ro,
{ ReadOnly => 1,
mysql_enable_utf8 => 1,
wait_timeout => 60*60*8,
mysql_auto_reconnect => 1 }
);
}
return $_ro_dbh;
}
return __PACKAGE__->db_Main();
}
sub utf8_all_columns {
my $class = shift;
$class->utf8_columns( $class->columns('All') );
}
sub utf8_columns {
my $class = shift;
# the default
$class->_utf8_columns([]) unless $class->_utf8_columns;
# a getter?
return @{ $class->_utf8_columns } unless @_;
my @columns = @_;
push @{ $class->_utf8_columns }, @columns;
$class->add_trigger($_ => sub {
my ($self) = @_;
for (@columns) {
next if ref($self->{$_});
utf8::upgrade( $self->{$_} ) if defined($self->{$_});
}
}) for qw( before_create before_update );
$class->add_trigger(select => sub {
my ($self) = @_;
for (@columns) {
next if ref($self->{$_});
if (defined($self->{$_})) {
# flip the bit..
Encode::_utf8_on($self->{$_});
utf8::decode($self->{$_});
# ..sanity check
if (!utf8::valid($self->{$_})) {
# if we're in an eval, let's at least not _completely_ stuff
# the process. Turn the bit off again.
Encode::_utf8_off($self->{$_});
# ..and die
$self->_log("Invalid UTF8 from database in column '$_': " . $self->{$_});
}
}
}
});
}
sub import {
my $class = shift;
local $Exporter::ExportLevel = 1;
if ($_[0] && $_[0] eq "-nosearch") {
shift; # ignore option
return $class->SUPER::import(@_);
}
if (caller(0)->isa('Class::DBI')) {
caller(0)->add_searcher(search => "Litmus::DBI::utf8Search");
}
$class->SUPER::import(@_);
}
#########################################################################
package Litmus::DBI::utf8Search;
use base 'Class::DBI::Search::Basic';
sub bind {
my $self = shift;
# for fast lookup of which cols are utf8
my %hash = map { $_ => 1 } $self->class->utf8_columns;
# get name => values of columns to search for
my $search_for = $self->_search_for();
# make an array that says whether the value at that position should be
# upgraded to utf8. This relies on ->bind() sorting the keys from _search_for()
# in the same way.
my @utf8cols = map { $hash{$_} && defined($search_for->{$_}) } sort keys %$search_for;
# take copy of array to avoid upgrading the original values; we only want to
# upgrade the values for the search.
my @bind = @{ $self->SUPER::bind(@_) };
my $i = 0;
for (@bind) {
if (shift @utf8cols) {
my $copy = $_;
utf8::upgrade($copy);
$bind[$i] = $copy;
}
$i++;
}
\@bind;
}
1;