# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
-#
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
# (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
# 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
=head1 METHODS
-=begin testing
-
-ok (require RT::SearchBuilder);
-
-=end testing
=cut
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;
}
$self->SUPER::_Init( 'Handle' => $RT::Handle);
}
-# }}}
-# {{{ sub LimitToEnabled
+sub _Handle { return $RT::Handle }
-=head2 LimitToEnabled
+sub CleanSlate {
+ my $self = shift;
+ $self->{'_sql_aliases'} = {};
+ delete $self->{'handled_disabled_column'};
+ delete $self->{'find_disabled_rows'};
+ return $self->SUPER::CleanSlate(@_);
+}
-Only find items that haven\'t been disabled
+sub JoinTransactions {
+ my $self = shift;
+ my %args = ( New => 0, @_ );
-=cut
+ return $self->{'_sql_aliases'}{'transactions'}
+ if !$args{'New'} && $self->{'_sql_aliases'}{'transactions'};
-sub LimitToEnabled {
- my $self = shift;
-
- $self->Limit( FIELD => 'Disabled',
- VALUE => '0',
- OPERATOR => '=' );
-}
-# }}}
+ my $alias = $self->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Transactions',
+ FIELD2 => 'ObjectId',
+ );
-# {{{ sub LimitToDisabled
+ my $item = $self->NewItem;
+ my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
-=head2 LimitToDeleted
+ $self->RT::SearchBuilder::Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'ObjectType',
+ VALUE => $object_type,
+ );
+ $self->{'_sql_aliases'}{'transactions'} = $alias
+ unless $args{'New'};
-Only find items that have been deleted.
+ return $alias;
+}
-=cut
+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 );
+}
-sub LimitToDeleted {
+# If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
+sub RowsPerPage {
my $self = shift;
-
- $self->{'find_disabled_rows'} = 1;
- $self->Limit( FIELD => 'Disabled',
- OPERATOR => '=',
- VALUE => '1'
- );
+ return if @_ and defined $_[0] and $_[0] =~ /\D/;
+ return $self->SUPER::RowsPerPage(@_);
}
-# }}}
-# {{{ sub LimitAttribute
+sub FirstRow {
+ my $self = shift;
+ return if @_ and defined $_[0] and $_[0] =~ /\D/;
+ return $self->SUPER::FirstRow(@_);
+}
-=head2 LimitAttribute PARAMHASH
+=head2 LimitToEnabled
-Takes NAME, OPERATOR and VALUE to find records that has the
-matching Attribute.
+Only find items that haven't been disabled
-If EMPTY is set, also select rows with an empty string as
-Attribute's Content.
+=cut
-If NULL is set, also select rows without the named Attribute.
+sub LimitToEnabled {
+ my $self = shift;
+
+ $self->{'handled_disabled_column'} = 1;
+ $self->Limit( FIELD => 'Disabled', VALUE => '0' );
+}
+
+=head2 LimitToDeleted
+
+Only find items that have been deleted.
=cut
-my %Negate = qw(
- = !=
- != =
- > <=
- < >=
- >= <
- <= >
- 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'
- );
+sub LimitToDeleted {
+ my $self = shift;
- my $type = ref($self);
- $type =~ s/(?:s|Collection)$//; # XXX - Hack!
+ $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
+ $self->Limit( FIELD => 'Disabled', VALUE => '1' );
+}
- $self->Limit(
- $clause => $alias,
- FIELD => 'ObjectType',
- OPERATOR => '=',
- VALUE => $type,
- );
- $self->Limit(
- $clause => $alias,
- FIELD => 'Name',
- OPERATOR => '=',
- VALUE => $args{NAME},
- ) if exists $args{NAME};
+=head2 FindAllRows
- return unless exists $args{VALUE};
+Find all matching rows, regardless of whether they are disabled or not
- $self->Limit(
- $clause => $alias,
- FIELD => 'Content',
- OPERATOR => $operator,
- VALUE => $args{VALUE},
- );
+=cut
- # 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;
}
-# }}}
-
-# {{{ sub LimitCustomField
=head2 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,
);
}
-# {{{ sub FindAllRows
-
-=head2 FindAllRows
-
-Find all matching rows, regardless of whether they are disabled or not
-
-=cut
-
-sub FindAllRows {
- shift->{'find_disabled_rows'} = 1;
-}
-
-# {{{ sub Limit
-
=head2 Limit PARAMHASH
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<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,
- @_ );
-
- return $self->SUPER::Limit(%args);
-}
+ 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';
+ }
-# {{{ sub ItemsOrderBy
+ 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 $items;
}
-# }}}
-
-# {{{ sub ItemsArrayRef
-
=head2 ItemsArrayRef
Return this object's ItemsArray, in the order that ItemsOrderBy sorts
it.
-=begin testing
+=cut
-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};
+sub ItemsArrayRef {
+ my $self = shift;
+ return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
+}
-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));
+# make sure that Disabled rows never get seen unless
+# we're explicitly trying to see them.
-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 _DoSearch {
+ my $self = shift;
-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");;
+ 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
-=end testing
+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 ItemsArrayRef {
+sub ColumnMapClassName {
my $self = shift;
- my @items;
-
- return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
+ my $Class = ref $self;
+ $Class =~ s/s$//;
+ $Class =~ s/:/_/g;
+ return $Class;
}
-# }}}
-
-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});
+RT::Base->_ImportOverlays();
1;
-
-