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