X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;ds=sidebyside;f=rt%2Flib%2FRT%2FSearchBuilder.pm;h=bfc0cd36b754a402de07cc4e2237a2caec6a593c;hb=919e930aa9279b3c5cd12b593889cd6de79d67bf;hp=527952b67ed800b3d060fab252e4a194de9204bc;hpb=624b2d44625f69d71175c3348cae635d580c890b;p=freeside.git diff --git a/rt/lib/RT/SearchBuilder.pm b/rt/lib/RT/SearchBuilder.pm index 527952b67..bfc0cd36b 100644 --- a/rt/lib/RT/SearchBuilder.pm +++ b/rt/lib/RT/SearchBuilder.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -43,7 +43,7 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # END BPS TAGGED BLOCK }}} =head1 NAME @@ -70,6 +70,7 @@ use DBIx::SearchBuilder "1.50"; use strict; use warnings; + use base qw(DBIx::SearchBuilder RT::Base); sub _Init { @@ -85,6 +86,68 @@ 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 @@ -93,10 +156,9 @@ Only find items that haven't been disabled sub LimitToEnabled { my $self = shift; - - $self->Limit( FIELD => 'Disabled', - VALUE => '0', - OPERATOR => '=' ); + + $self->{'handled_disabled_column'} = 1; + $self->Limit( FIELD => 'Disabled', VALUE => '0' ); } =head2 LimitToDeleted @@ -107,102 +169,19 @@ Only find items that have been deleted. sub LimitToDeleted { my $self = shift; - - $self->{'find_disabled_rows'} = 1; - $self->Limit( FIELD => 'Disabled', - OPERATOR => '=', - VALUE => '1' - ); -} - -=head2 LimitAttribute PARAMHASH -Takes NAME, OPERATOR and VALUE to find records that has the -matching Attribute. + $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1; + $self->Limit( FIELD => 'Disabled', VALUE => '1' ); +} -If EMPTY is set, also select rows with an empty string as -Attribute's Content. +=head2 FindAllRows -If NULL is set, also select rows without the named Attribute. +Find all matching rows, regardless of whether they are disabled or not =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}; +sub FindAllRows { + shift->{'find_disabled_rows'} = 1; } =head2 LimitCustomField @@ -236,40 +215,36 @@ sub LimitCustomField { @_ ); 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'}, + ALIAS => $alias, + FIELD => 'Content', + OPERATOR => $args{'OPERATOR'}, + VALUE => $args{'VALUE'}, + ); + $self->Limit( + ALIAS => $alias, + FIELD => 'Disabled', + OPERATOR => '=', + VALUE => 0, ); -} - -=head2 FindAllRows - -Find all matching rows, regardless of whether they are disabled or not - -=cut - -sub FindAllRows { - shift->{'find_disabled_rows'} = 1; } =head2 Limit PARAMHASH @@ -278,14 +253,48 @@ This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus 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 when the OPERATOR is C or C. +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 @@ -323,9 +332,48 @@ sub ItemsArrayRef { return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef()); } -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}); +# make sure that Disabled rows never get seen unless +# we're explicitly trying to see them. + +sub _DoSearch { + my $self = shift; + + if ( $self->{'with_disabled_column'} + && !$self->{'handled_disabled_column'} + && !$self->{'find_disabled_rows'} + ) { + $self->LimitToEnabled; + } + return $self->SUPER::_DoSearch(@_); +} +sub _DoCount { + my $self = shift; + + if ( $self->{'with_disabled_column'} + && !$self->{'handled_disabled_column'} + && !$self->{'find_disabled_rows'} + ) { + $self->LimitToEnabled; + } + return $self->SUPER::_DoCount(@_); +} + +=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;