#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
package RT::SearchBuilder;
use RT::Base;
-use DBIx::SearchBuilder "1.40";
+use DBIx::SearchBuilder "1.50";
use strict;
use warnings;
+
use base qw(DBIx::SearchBuilder RT::Base);
sub _Init {
$self->SUPER::_Init( 'Handle' => $RT::Handle);
}
+sub _Handle { return $RT::Handle }
+
+sub CleanSlate {
+ my $self = shift;
+ $self->{'_sql_aliases'} = {};
+ delete $self->{'handled_disabled_column'};
+ delete $self->{'find_disabled_rows'};
+ return $self->SUPER::CleanSlate(@_);
+}
+
+sub JoinTransactions {
+ my $self = shift;
+ my %args = ( New => 0, @_ );
+
+ return $self->{'_sql_aliases'}{'transactions'}
+ if !$args{'New'} && $self->{'_sql_aliases'}{'transactions'};
+
+ my $alias = $self->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Transactions',
+ FIELD2 => 'ObjectId',
+ );
+
+ my $item = $self->NewItem;
+ my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
+
+ $self->RT::SearchBuilder::Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'ObjectType',
+ VALUE => $object_type,
+ );
+ $self->{'_sql_aliases'}{'transactions'} = $alias
+ unless $args{'New'};
+
+ return $alias;
+}
+
+sub OrderByCols {
+ my $self = shift;
+ my @sort;
+ for my $s (@_) {
+ next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/;
+ $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION};
+ push @sort, $s;
+ }
+ return $self->SUPER::OrderByCols( @sort );
+}
+
+# If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
+sub RowsPerPage {
+ my $self = shift;
+ return if @_ and defined $_[0] and $_[0] =~ /\D/;
+ return $self->SUPER::RowsPerPage(@_);
+}
+
+sub FirstRow {
+ my $self = shift;
+ return if @_ and defined $_[0] and $_[0] =~ /\D/;
+ return $self->SUPER::FirstRow(@_);
+}
+
=head2 LimitToEnabled
Only find items that haven't been disabled
shift->{'find_disabled_rows'} = 1;
}
-=head2 LimitAttribute PARAMHASH
-
-Takes NAME, OPERATOR and VALUE to find records that has the
-matching Attribute.
-
-If EMPTY is set, also select rows with an empty string as
-Attribute's Content.
-
-If NULL is set, also select rows without the named Attribute.
-
-=cut
-
-my %Negate = (
- '=' => '!=',
- '!=' => '=',
- '>' => '<=',
- '<' => '>=',
- '>=' => '<',
- '<=' => '>',
- 'LIKE' => 'NOT LIKE',
- 'NOT LIKE' => 'LIKE',
- 'IS' => 'IS NOT',
- 'IS NOT' => 'IS',
-);
-
-sub LimitAttribute {
- my ($self, %args) = @_;
- my $clause = 'ALIAS';
- my $operator = ($args{OPERATOR} || '=');
-
- if ($args{NULL} and exists $args{VALUE}) {
- $clause = 'LEFTJOIN';
- $operator = $Negate{$operator};
- }
- elsif ($args{NEGATE}) {
- $operator = $Negate{$operator};
- }
-
- my $alias = $self->Join(
- TYPE => 'left',
- ALIAS1 => $args{ALIAS} || 'main',
- FIELD1 => 'id',
- TABLE2 => 'Attributes',
- FIELD2 => 'ObjectId'
- );
-
- my $type = ref($self);
- $type =~ s/(?:s|Collection)$//; # XXX - Hack!
-
- $self->Limit(
- $clause => $alias,
- FIELD => 'ObjectType',
- OPERATOR => '=',
- VALUE => $type,
- );
- $self->Limit(
- $clause => $alias,
- FIELD => 'Name',
- OPERATOR => '=',
- VALUE => $args{NAME},
- ) if exists $args{NAME};
-
- return unless exists $args{VALUE};
-
- $self->Limit(
- $clause => $alias,
- FIELD => 'Content',
- OPERATOR => $operator,
- VALUE => $args{VALUE},
- );
-
- # Capture rows with the attribute defined as an empty string.
- $self->Limit(
- $clause => $alias,
- FIELD => 'Content',
- OPERATOR => '=',
- VALUE => '',
- ENTRYAGGREGATOR => $args{NULL} ? 'AND' : 'OR',
- ) if $args{EMPTY};
-
- # Capture rows without the attribute defined
- $self->Limit(
- %args,
- ALIAS => $alias,
- FIELD => 'id',
- OPERATOR => ($args{NEGATE} ? 'IS NOT' : 'IS'),
- VALUE => 'NULL',
- ) if $args{NULL};
-}
-
=head2 LimitCustomField
Takes a paramhash of key/value pairs with the following keys:
@_ );
my $alias = $self->Join(
- TYPE => 'left',
- ALIAS1 => 'main',
- FIELD1 => 'id',
- TABLE2 => 'ObjectCustomFieldValues',
- FIELD2 => 'ObjectId'
+ TYPE => 'left',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'ObjectId'
);
$self->Limit(
- ALIAS => $alias,
- FIELD => 'CustomField',
- OPERATOR => '=',
- VALUE => $args{'CUSTOMFIELD'},
+ ALIAS => $alias,
+ FIELD => 'CustomField',
+ OPERATOR => '=',
+ VALUE => $args{'CUSTOMFIELD'},
) if ($args{'CUSTOMFIELD'});
$self->Limit(
- ALIAS => $alias,
- FIELD => 'ObjectType',
- OPERATOR => '=',
- VALUE => $self->_SingularClass,
+ ALIAS => $alias,
+ FIELD => 'ObjectType',
+ OPERATOR => '=',
+ VALUE => $self->_SingularClass,
+ );
+ $self->Limit(
+ ALIAS => $alias,
+ FIELD => 'Content',
+ OPERATOR => $args{'OPERATOR'},
+ VALUE => $args{'VALUE'},
);
$self->Limit(
- ALIAS => $alias,
- FIELD => 'Content',
- OPERATOR => $args{'OPERATOR'},
- VALUE => $args{'VALUE'},
+ ALIAS => $alias,
+ FIELD => 'Disabled',
+ OPERATOR => '=',
+ VALUE => 0,
);
}
making sure that by default lots of things don't do extra work trying to
match lower(colname) agaist lc($val);
+We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
+This ensures that we don't pass invalid SQL to the database or allow SQL
+injection attacks when we pass through user specified values.
+
=cut
sub Limit {
my $self = shift;
- my %args = ( CASESENSITIVE => 1,
- @_ );
+ my %ARGS = (
+ CASESENSITIVE => 1,
+ OPERATOR => '=',
+ @_,
+ );
+
+ # We use the same regex here that DBIx::SearchBuilder uses to exclude
+ # values from quoting
+ if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
+ # Don't pass anything but NULL for IS and IS NOT
+ $ARGS{'VALUE'} = 'NULL';
+ }
- return $self->SUPER::Limit(%args);
+ if ($ARGS{FUNCTION}) {
+ ($ARGS{ALIAS}, $ARGS{FIELD}) = split /\./, delete $ARGS{FUNCTION}, 2;
+ $self->SUPER::Limit(%ARGS);
+ } elsif ($ARGS{FIELD} =~ /\W/
+ or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
+ |(NOT\s*)?LIKE
+ |(NOT\s*)?(STARTS|ENDS)WITH
+ |(NOT\s*)?MATCHES
+ |IS(\s*NOT)?
+ |(NOT\s*)?IN
+ |\@\@)$/ix) {
+ $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
+ $self->SUPER::Limit(
+ %ARGS,
+ FIELD => 'id',
+ OPERATOR => '<',
+ VALUE => '0',
+ );
+ } else {
+ $self->SUPER::Limit(%ARGS);
+ }
}
=head2 ItemsOrderBy
return $self->SUPER::_DoCount(@_);
}
-eval "require RT::SearchBuilder_Vendor";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/SearchBuilder_Vendor.pm});
-eval "require RT::SearchBuilder_Local";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/SearchBuilder_Local.pm});
+=head2 ColumnMapClassName
+
+ColumnMap needs a Collection name to load the correct list display.
+Depluralization is hard, so provide an easy way to correct the naive
+algorithm that this code uses.
+
+=cut
+
+sub ColumnMapClassName {
+ my $self = shift;
+ my $Class = ref $self;
+ $Class =~ s/s$//;
+ $Class =~ s/:/_/g;
+ return $Class;
+}
+
+RT::Base->_ImportOverlays();
1;