X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FSearchBuilder.pm;h=8b808c69b2218136f27c0ae6ded501e0441c0631;hb=b8988e1d3ac75af63c85e8563e57701030315a9e;hp=22c9aff8cf11a14408f46aaf7e2d382448a329c9;hpb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d;p=freeside.git diff --git a/rt/lib/RT/SearchBuilder.pm b/rt/lib/RT/SearchBuilder.pm index 22c9aff8c..8b808c69b 100644 --- a/rt/lib/RT/SearchBuilder.pm +++ b/rt/lib/RT/SearchBuilder.pm @@ -1,26 +1,51 @@ -# BEGIN LICENSE BLOCK -# -# Copyright (c) 1996-2003 Jesse Vincent -# -# (Except where explictly superceded by other copyright notices) -# +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2014 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. -# -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. -# -# -# END LICENSE BLOCK +# +# 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 +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# 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 RT::SearchBuilder - a baseclass for RT collection objects @@ -33,11 +58,6 @@ =head1 METHODS -=begin testing - -ok (require RT::SearchBuilder); - -=end testing =cut @@ -45,13 +65,14 @@ ok (require RT::SearchBuilder); package RT::SearchBuilder; use RT::Base; -use DBIx::SearchBuilder; +use DBIx::SearchBuilder "1.50"; use strict; -use vars qw(@ISA); -@ISA = qw(DBIx::SearchBuilder RT::Base); +use warnings; + + +use base qw(DBIx::SearchBuilder RT::Base); -# {{{ sub _Init sub _Init { my $self = shift; @@ -64,26 +85,81 @@ sub _Init { } $self->SUPER::_Init( 'Handle' => $RT::Handle); } -# }}} -# {{{ sub LimitToEnabled +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 +Only find items that haven't been disabled =cut sub LimitToEnabled { my $self = shift; - - $self->Limit( FIELD => 'Disabled', - VALUE => '0', - OPERATOR => '=' ); -} -# }}} -# {{{ sub LimitToDisabled + $self->{'handled_disabled_column'} = 1; + $self->Limit( FIELD => 'Disabled', VALUE => '0' ); +} =head2 LimitToDeleted @@ -93,16 +169,10 @@ Only find items that have been deleted. sub LimitToDeleted { my $self = shift; - - $self->{'find_disabled_rows'} = 1; - $self->Limit( FIELD => 'Disabled', - OPERATOR => '=', - VALUE => '1' - ); -} -# }}} -# {{{ sub FindAllRows + $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1; + $self->Limit( FIELD => 'Disabled', VALUE => '1' ); +} =head2 FindAllRows @@ -111,10 +181,71 @@ Find all matching rows, regardless of whether they are disabled or not =cut sub FindAllRows { - shift->{'find_disabled_rows'} = 1; + shift->{'find_disabled_rows'} = 1; } -# {{{ sub Limit +=head2 LimitCustomField + +Takes a paramhash of key/value pairs with the following keys: + +=over 4 + +=item CUSTOMFIELD - CustomField id. Optional + +=item OPERATOR - The usual Limit operators + +=item VALUE - The value to compare against + +=back + +=cut + +sub _SingularClass { + my $self = shift; + my $class = ref($self); + $class =~ s/s$// or die "Cannot deduce SingularClass for $class"; + return $class; +} + +sub LimitCustomField { + my $self = shift; + my %args = ( VALUE => undef, + CUSTOMFIELD => undef, + OPERATOR => '=', + @_ ); + + my $alias = $self->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFieldValues', + FIELD2 => 'ObjectId' + ); + $self->Limit( + ALIAS => $alias, + FIELD => 'CustomField', + OPERATOR => '=', + VALUE => $args{'CUSTOMFIELD'}, + ) if ($args{'CUSTOMFIELD'}); + $self->Limit( + 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 => 'Disabled', + OPERATOR => '=', + VALUE => 0, + ); +} =head2 Limit PARAMHASH @@ -122,79 +253,127 @@ 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 $self = shift; + 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); + } } -# }}} - -# {{{ sub ItemsArrayRef - -=item ItemsArrayRef +=head2 ItemsOrderBy -Return this object's ItemsArray. If it has a SortOrder attribute, sort the array by SortOrder. Otherwise, if it has a "Name" attribute, sort alphabetically by Name -Otherwise, just give up and return it in the order it came from the db. - - -=begin testing +Otherwise, just give up and return it in the order it came from the +db. -use_ok(RT::Queues); -ok(my $queues = RT::Queues->new($RT::SystemUser), 'Created a queues object'); -ok( $queues->UnLimit(),'Unlimited the result set of the queues object'); -my $items = $queues->ItemsArrayRef(); -my @items = @{$items}; - -ok($queues->NewItem->_Accessible('Name','read')); -my @sorted = sort {lc($a->Name) cmp lc($b->Name)} @items; -ok (@sorted, "We have an array of queues, sorted". join(',',map {$_->Name} @sorted)); +=cut -ok (@items, "We have an array of queues, raw". join(',',map {$_->Name} @items)); -my @sorted_ids = map {$_->id } @sorted; -my @items_ids = map {$_->id } @items; +sub ItemsOrderBy { + my $self = shift; + my $items = shift; + + if ($self->NewItem()->_Accessible('SortOrder','read')) { + $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ]; + } + elsif ($self->NewItem()->_Accessible('Name','read')) { + $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ]; + } -is ($#sorted, $#items); -is ($sorted[0]->Name, $items[0]->Name); -is ($sorted[-1]->Name, $items[-1]->Name); -is_deeply(\@items_ids, \@sorted_ids, "ItemsArrayRef sorts alphabetically by name");; + return $items; +} +=head2 ItemsArrayRef -=end testing +Return this object's ItemsArray, in the order that ItemsOrderBy sorts +it. =cut sub ItemsArrayRef { my $self = shift; - my @items; - - if ($self->NewItem()->_Accessible('SortOrder','read')) { - @items = sort { $a->SortOrder <=> $b->SortOrder } @{$self->SUPER::ItemsArrayRef()}; - } - elsif ($self->NewItem()->_Accessible('Name','read')) { - @items = sort { lc($a->Name) cmp lc($b->Name) } @{$self->SUPER::ItemsArrayRef()}; - } - else { - @items = @{$self->SUPER::ItemsArrayRef()}; - } + return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef()); +} + +# make sure that Disabled rows never get seen unless +# we're explicitly trying to see them. - return(\@items); +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(@_); +} -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 -1; +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;