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