405a8fdc0af11c0d7d20373be4a74ca5f54883ce
[freeside.git] / rt / lib / RT / SearchBuilder.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 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.50";
69
70 use strict;
71 use warnings;
72
73 use base qw(DBIx::SearchBuilder RT::Base);
74
75 sub _Init  {
76     my $self = shift;
77     
78     $self->{'user'} = shift;
79     unless(defined($self->CurrentUser)) {
80         use Carp;
81         Carp::confess("$self was created without a CurrentUser");
82         $RT::Logger->err("$self was created without a CurrentUser");
83         return(0);
84     }
85     $self->SUPER::_Init( 'Handle' => $RT::Handle);
86 }
87
88 sub OrderByCols {
89     my $self = shift;
90     my @sort;
91     for my $s (@_) {
92         next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/;
93         $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION};
94         push @sort, $s;
95     }
96     return $self->SUPER::OrderByCols( @sort );
97 }
98
99 # If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
100 sub RowsPerPage {
101     my $self = shift;
102     return if @_ and defined $_[0] and $_[0] =~ /\D/;
103     return $self->SUPER::RowsPerPage(@_);
104 }
105
106 sub FirstRow {
107     my $self = shift;
108     return if @_ and defined $_[0] and $_[0] =~ /\D/;
109     return $self->SUPER::FirstRow(@_);
110 }
111
112 =head2 LimitToEnabled
113
114 Only find items that haven't been disabled
115
116 =cut
117
118 sub LimitToEnabled {
119     my $self = shift;
120
121     $self->{'handled_disabled_column'} = 1;
122     $self->Limit( FIELD => 'Disabled', VALUE => '0' );
123 }
124
125 =head2 LimitToDeleted
126
127 Only find items that have been deleted.
128
129 =cut
130
131 sub LimitToDeleted {
132     my $self = shift;
133
134     $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
135     $self->Limit( FIELD => 'Disabled', VALUE => '1' );
136 }
137
138 =head2 FindAllRows
139
140 Find all matching rows, regardless of whether they are disabled or not
141
142 =cut
143
144 sub FindAllRows {
145     shift->{'find_disabled_rows'} = 1;
146 }
147
148 =head2 LimitAttribute PARAMHASH
149
150 Takes NAME, OPERATOR and VALUE to find records that has the
151 matching Attribute.
152
153 If EMPTY is set, also select rows with an empty string as
154 Attribute's Content.
155
156 If NULL is set, also select rows without the named Attribute.
157
158 =cut
159
160 my %Negate = (
161     '='        => '!=',
162     '!='       => '=',
163     '>'        => '<=',
164     '<'        => '>=',
165     '>='       => '<',
166     '<='       => '>',
167     'LIKE'     => 'NOT LIKE',
168     'NOT LIKE' => 'LIKE',
169     'IS'       => 'IS NOT',
170     'IS NOT'   => 'IS',
171 );
172
173 sub LimitAttribute {
174     my ($self, %args) = @_;
175     my $clause = 'ALIAS';
176     my $operator = ($args{OPERATOR} || '=');
177     
178     if ($args{NULL} and exists $args{VALUE}) {
179         $clause = 'LEFTJOIN';
180         $operator = $Negate{$operator};
181     }
182     elsif ($args{NEGATE}) {
183         $operator = $Negate{$operator};
184     }
185     
186     my $alias = $self->Join(
187         TYPE   => 'left',
188         ALIAS1 => $args{ALIAS} || 'main',
189         FIELD1 => 'id',
190         TABLE2 => 'Attributes',
191         FIELD2 => 'ObjectId'
192     );
193
194     my $type = ref($self);
195     $type =~ s/(?:s|Collection)$//; # XXX - Hack!
196
197     $self->Limit(
198         $clause    => $alias,
199         FIELD      => 'ObjectType',
200         OPERATOR   => '=',
201         VALUE      => $type,
202     );
203     $self->Limit(
204         $clause    => $alias,
205         FIELD      => 'Name',
206         OPERATOR   => '=',
207         VALUE      => $args{NAME},
208     ) if exists $args{NAME};
209
210     return unless exists $args{VALUE};
211
212     $self->Limit(
213         $clause    => $alias,
214         FIELD      => 'Content',
215         OPERATOR   => $operator,
216         VALUE      => $args{VALUE},
217     );
218
219     # Capture rows with the attribute defined as an empty string.
220     $self->Limit(
221         $clause    => $alias,
222         FIELD      => 'Content',
223         OPERATOR   => '=',
224         VALUE      => '',
225         ENTRYAGGREGATOR => $args{NULL} ? 'AND' : 'OR',
226     ) if $args{EMPTY};
227
228     # Capture rows without the attribute defined
229     $self->Limit(
230         %args,
231         ALIAS      => $alias,
232         FIELD      => 'id',
233         OPERATOR   => ($args{NEGATE} ? 'IS NOT' : 'IS'),
234         VALUE      => 'NULL',
235     ) if $args{NULL};
236 }
237
238 =head2 LimitCustomField
239
240 Takes a paramhash of key/value pairs with the following keys:
241
242 =over 4
243
244 =item CUSTOMFIELD - CustomField id. Optional
245
246 =item OPERATOR - The usual Limit operators
247
248 =item VALUE - The value to compare against
249
250 =back
251
252 =cut
253
254 sub _SingularClass {
255     my $self = shift;
256     my $class = ref($self);
257     $class =~ s/s$// or die "Cannot deduce SingularClass for $class";
258     return $class;
259 }
260
261 sub LimitCustomField {
262     my $self = shift;
263     my %args = ( VALUE        => undef,
264                  CUSTOMFIELD  => undef,
265                  OPERATOR     => '=',
266                  @_ );
267
268     my $alias = $self->Join(
269         TYPE       => 'left',
270         ALIAS1     => 'main',
271         FIELD1     => 'id',
272         TABLE2     => 'ObjectCustomFieldValues',
273         FIELD2     => 'ObjectId'
274     );
275     $self->Limit(
276         ALIAS      => $alias,
277         FIELD      => 'CustomField',
278         OPERATOR   => '=',
279         VALUE      => $args{'CUSTOMFIELD'},
280     ) if ($args{'CUSTOMFIELD'});
281     $self->Limit(
282         ALIAS      => $alias,
283         FIELD      => 'ObjectType',
284         OPERATOR   => '=',
285         VALUE      => $self->_SingularClass,
286     );
287     $self->Limit(
288         ALIAS      => $alias,
289         FIELD      => 'Content',
290         OPERATOR   => $args{'OPERATOR'},
291         VALUE      => $args{'VALUE'},
292     );
293 }
294
295 =head2 Limit PARAMHASH
296
297 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
298 making sure that by default lots of things don't do extra work trying to 
299 match lower(colname) agaist lc($val);
300
301 We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
302 This ensures that we don't pass invalid SQL to the database or allow SQL
303 injection attacks when we pass through user specified values.
304
305 =cut
306
307 sub Limit {
308     my $self = shift;
309     my %ARGS = (
310         CASESENSITIVE => 1,
311         OPERATOR => '=',
312         @_,
313     );
314
315     # We use the same regex here that DBIx::SearchBuilder uses to exclude
316     # values from quoting
317     if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
318         # Don't pass anything but NULL for IS and IS NOT
319         $ARGS{'VALUE'} = 'NULL';
320     }
321
322     if ($ARGS{FUNCTION}) {
323         ($ARGS{ALIAS}, $ARGS{FIELD}) = split /\./, delete $ARGS{FUNCTION}, 2;
324         $self->SUPER::Limit(%ARGS);
325     } elsif ($ARGS{FIELD} =~ /\W/
326           or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
327                                   |(NOT\s*)?LIKE
328                                   |(NOT\s*)?(STARTS|ENDS)WITH
329                                   |(NOT\s*)?MATCHES
330                                   |IS(\s*NOT)?
331                                   |IN)$/ix) {
332         $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
333         $self->SUPER::Limit(
334             %ARGS,
335             FIELD    => 'id',
336             OPERATOR => '<',
337             VALUE    => '0',
338         );
339     } else {
340         $self->SUPER::Limit(%ARGS);
341     }
342 }
343
344 =head2 ItemsOrderBy
345
346 If it has a SortOrder attribute, sort the array by SortOrder.
347 Otherwise, if it has a "Name" attribute, sort alphabetically by Name
348 Otherwise, just give up and return it in the order it came from the
349 db.
350
351 =cut
352
353 sub ItemsOrderBy {
354     my $self = shift;
355     my $items = shift;
356   
357     if ($self->NewItem()->_Accessible('SortOrder','read')) {
358         $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ];
359     }
360     elsif ($self->NewItem()->_Accessible('Name','read')) {
361         $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ];
362     }
363
364     return $items;
365 }
366
367 =head2 ItemsArrayRef
368
369 Return this object's ItemsArray, in the order that ItemsOrderBy sorts
370 it.
371
372 =cut
373
374 sub ItemsArrayRef {
375     my $self = shift;
376     return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
377 }
378
379 # make sure that Disabled rows never get seen unless
380 # we're explicitly trying to see them.
381
382 sub _DoSearch {
383     my $self = shift;
384
385     if ( $self->{'with_disabled_column'}
386         && !$self->{'handled_disabled_column'}
387         && !$self->{'find_disabled_rows'}
388     ) {
389         $self->LimitToEnabled;
390     }
391     return $self->SUPER::_DoSearch(@_);
392 }
393 sub _DoCount {
394     my $self = shift;
395
396     if ( $self->{'with_disabled_column'}
397         && !$self->{'handled_disabled_column'}
398         && !$self->{'find_disabled_rows'}
399     ) {
400         $self->LimitToEnabled;
401     }
402     return $self->SUPER::_DoCount(@_);
403 }
404
405 RT::Base->_ImportOverlays();
406
407 1;