51b894fff86a6d11c81bf1bf21cbb54a6f76b9c3
[usit-rt.git] / lib / RT / SearchBuilder.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::SearchBuilder - a baseclass for RT collection objects
52
53 =head1 SYNOPSIS
54
55 =head1 DESCRIPTION
56
57
58 =head1 METHODS
59
60
61
62
63 =cut
64
65 package RT::SearchBuilder;
66
67 use RT::Base;
68 use DBIx::SearchBuilder "1.40";
69
70 use strict;
71 use warnings;
72
73
74 use base qw(DBIx::SearchBuilder RT::Base);
75
76 sub _Init  {
77     my $self = shift;
78     
79     $self->{'user'} = shift;
80     unless(defined($self->CurrentUser)) {
81         use Carp;
82         Carp::confess("$self was created without a CurrentUser");
83         $RT::Logger->err("$self was created without a CurrentUser");
84         return(0);
85     }
86     $self->SUPER::_Init( 'Handle' => $RT::Handle);
87 }
88
89 sub _Handle { return $RT::Handle }
90
91 sub CleanSlate {
92     my $self = shift;
93     $self->{'_sql_aliases'} = {};
94     delete $self->{'handled_disabled_column'};
95     delete $self->{'find_disabled_rows'};
96     return $self->SUPER::CleanSlate(@_);
97 }
98
99 sub JoinTransactions {
100     my $self = shift;
101     my %args = ( New => 0, @_ );
102
103     return $self->{'_sql_aliases'}{'transactions'}
104         if !$args{'New'} && $self->{'_sql_aliases'}{'transactions'};
105
106     my $alias = $self->Join(
107         ALIAS1 => 'main',
108         FIELD1 => 'id',
109         TABLE2 => 'Transactions',
110         FIELD2 => 'ObjectId',
111     );
112
113     my $item = $self->NewItem;
114     my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
115
116     $self->RT::SearchBuilder::Limit(
117         LEFTJOIN => $alias,
118         FIELD    => 'ObjectType',
119         VALUE    => $object_type,
120     );
121     $self->{'_sql_aliases'}{'transactions'} = $alias
122         unless $args{'New'};
123
124     return $alias;
125 }
126
127 sub OrderByCols {
128     my $self = shift;
129     my @sort;
130     for my $s (@_) {
131         next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/;
132         $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION};
133         push @sort, $s;
134     }
135     return $self->SUPER::OrderByCols( @sort );
136 }
137
138 # If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
139 sub RowsPerPage {
140     my $self = shift;
141     return if @_ and defined $_[0] and $_[0] =~ /\D/;
142     return $self->SUPER::RowsPerPage(@_);
143 }
144
145 sub FirstRow {
146     my $self = shift;
147     return if @_ and defined $_[0] and $_[0] =~ /\D/;
148     return $self->SUPER::FirstRow(@_);
149 }
150
151 =head2 LimitToEnabled
152
153 Only find items that haven't been disabled
154
155 =cut
156
157 sub LimitToEnabled {
158     my $self = shift;
159
160     $self->{'handled_disabled_column'} = 1;
161     $self->Limit( FIELD => 'Disabled', VALUE => '0' );
162 }
163
164 =head2 LimitToDeleted
165
166 Only find items that have been deleted.
167
168 =cut
169
170 sub LimitToDeleted {
171     my $self = shift;
172
173     $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
174     $self->Limit( FIELD => 'Disabled', VALUE => '1' );
175 }
176
177 =head2 FindAllRows
178
179 Find all matching rows, regardless of whether they are disabled or not
180
181 =cut
182
183 sub FindAllRows {
184     shift->{'find_disabled_rows'} = 1;
185 }
186
187 =head2 LimitCustomField
188
189 Takes a paramhash of key/value pairs with the following keys:
190
191 =over 4
192
193 =item CUSTOMFIELD - CustomField id. Optional
194
195 =item OPERATOR - The usual Limit operators
196
197 =item VALUE - The value to compare against
198
199 =back
200
201 =cut
202
203 sub _SingularClass {
204     my $self = shift;
205     my $class = ref($self);
206     $class =~ s/s$// or die "Cannot deduce SingularClass for $class";
207     return $class;
208 }
209
210 sub LimitCustomField {
211     my $self = shift;
212     my %args = ( VALUE        => undef,
213                  CUSTOMFIELD  => undef,
214                  OPERATOR     => '=',
215                  @_ );
216
217     my $alias = $self->Join(
218         TYPE       => 'left',
219         ALIAS1     => 'main',
220         FIELD1     => 'id',
221         TABLE2     => 'ObjectCustomFieldValues',
222         FIELD2     => 'ObjectId'
223     );
224     $self->Limit(
225         ALIAS      => $alias,
226         FIELD      => 'CustomField',
227         OPERATOR   => '=',
228         VALUE      => $args{'CUSTOMFIELD'},
229     ) if ($args{'CUSTOMFIELD'});
230     $self->Limit(
231         ALIAS      => $alias,
232         FIELD      => 'ObjectType',
233         OPERATOR   => '=',
234         VALUE      => $self->_SingularClass,
235     );
236     $self->Limit(
237         ALIAS      => $alias,
238         FIELD      => 'Content',
239         OPERATOR   => $args{'OPERATOR'},
240         VALUE      => $args{'VALUE'},
241     );
242     $self->Limit(
243         ALIAS => $alias,
244         FIELD => 'Disabled',
245         OPERATOR => '=',
246         VALUE => 0,
247     );
248 }
249
250 =head2 Limit PARAMHASH
251
252 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
253 making sure that by default lots of things don't do extra work trying to 
254 match lower(colname) agaist lc($val);
255
256 We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
257 This ensures that we don't pass invalid SQL to the database or allow SQL
258 injection attacks when we pass through user specified values.
259
260 =cut
261
262 sub Limit {
263     my $self = shift;
264     my %ARGS = (
265         CASESENSITIVE => 1,
266         OPERATOR => '=',
267         @_,
268     );
269
270     # We use the same regex here that DBIx::SearchBuilder uses to exclude
271     # values from quoting
272     if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
273         # Don't pass anything but NULL for IS and IS NOT
274         $ARGS{'VALUE'} = 'NULL';
275     }
276
277     if ($ARGS{FUNCTION}) {
278         ($ARGS{ALIAS}, $ARGS{FIELD}) = split /\./, delete $ARGS{FUNCTION}, 2;
279         $self->SUPER::Limit(%ARGS);
280     } elsif ($ARGS{FIELD} =~ /\W/
281           or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
282                                   |(NOT\s*)?LIKE
283                                   |(NOT\s*)?(STARTS|ENDS)WITH
284                                   |(NOT\s*)?MATCHES
285                                   |IS(\s*NOT)?
286                                   |(NOT\s*)?IN
287                                   |\@\@)$/ix) {
288         $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
289         $self->SUPER::Limit(
290             %ARGS,
291             FIELD    => 'id',
292             OPERATOR => '<',
293             VALUE    => '0',
294         );
295     } else {
296         $self->SUPER::Limit(%ARGS);
297     }
298 }
299
300 =head2 ItemsOrderBy
301
302 If it has a SortOrder attribute, sort the array by SortOrder.
303 Otherwise, if it has a "Name" attribute, sort alphabetically by Name
304 Otherwise, just give up and return it in the order it came from the
305 db.
306
307 =cut
308
309 sub ItemsOrderBy {
310     my $self = shift;
311     my $items = shift;
312   
313     if ($self->NewItem()->_Accessible('SortOrder','read')) {
314         $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ];
315     }
316     elsif ($self->NewItem()->_Accessible('Name','read')) {
317         $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ];
318     }
319
320     return $items;
321 }
322
323 =head2 ItemsArrayRef
324
325 Return this object's ItemsArray, in the order that ItemsOrderBy sorts
326 it.
327
328 =cut
329
330 sub ItemsArrayRef {
331     my $self = shift;
332     return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
333 }
334
335 # make sure that Disabled rows never get seen unless
336 # we're explicitly trying to see them.
337
338 sub _DoSearch {
339     my $self = shift;
340
341     if ( $self->{'with_disabled_column'}
342         && !$self->{'handled_disabled_column'}
343         && !$self->{'find_disabled_rows'}
344     ) {
345         $self->LimitToEnabled;
346     }
347     return $self->SUPER::_DoSearch(@_);
348 }
349 sub _DoCount {
350     my $self = shift;
351
352     if ( $self->{'with_disabled_column'}
353         && !$self->{'handled_disabled_column'}
354         && !$self->{'find_disabled_rows'}
355     ) {
356         $self->LimitToEnabled;
357     }
358     return $self->SUPER::_DoCount(@_);
359 }
360
361 =head2 ColumnMapClassName
362
363 ColumnMap needs a Collection name to load the correct list display.
364 Depluralization is hard, so provide an easy way to correct the naive
365 algorithm that this code uses.
366
367 =cut
368
369 sub ColumnMapClassName {
370     my $self = shift;
371     my $Class = ref $self;
372     $Class =~ s/s$//;
373     $Class =~ s/:/_/g;
374     return $Class;
375 }
376
377 RT::Base->_ImportOverlays();
378
379 1;