]> git.uio.no Git - usit-rt.git/blob - lib/RT/SearchBuilder.pm
Upgrade to 4.2.8
[usit-rt.git] / lib / RT / SearchBuilder.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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 strict;
68 use warnings;
69 use 5.010;
70
71 use base qw(DBIx::SearchBuilder RT::Base);
72
73 use RT::Base;
74 use DBIx::SearchBuilder "1.40";
75
76 use Scalar::Util qw/blessed/;
77
78 sub _Init  {
79     my $self = shift;
80     
81     $self->{'user'} = shift;
82     unless(defined($self->CurrentUser)) {
83         use Carp;
84         Carp::confess("$self was created without a CurrentUser");
85         $RT::Logger->err("$self was created without a CurrentUser");
86         return(0);
87     }
88     $self->SUPER::_Init( 'Handle' => $RT::Handle);
89 }
90
91 sub _Handle { return $RT::Handle }
92
93 sub CleanSlate {
94     my $self = shift;
95     $self->{'_sql_aliases'} = {};
96     delete $self->{'handled_disabled_column'};
97     delete $self->{'find_disabled_rows'};
98     return $self->SUPER::CleanSlate(@_);
99 }
100
101 sub Join {
102     my $self = shift;
103     my %args = @_;
104
105     $args{'DISTINCT'} = 1 if
106         !exists $args{'DISTINCT'}
107         && $args{'TABLE2'} && lc($args{'FIELD2'}||'') eq 'id';
108
109     return $self->SUPER::Join( %args );
110 }
111
112 sub JoinTransactions {
113     my $self = shift;
114     my %args = ( New => 0, @_ );
115
116     return $self->{'_sql_aliases'}{'transactions'}
117         if !$args{'New'} && $self->{'_sql_aliases'}{'transactions'};
118
119     my $alias = $self->Join(
120         ALIAS1 => 'main',
121         FIELD1 => 'id',
122         TABLE2 => 'Transactions',
123         FIELD2 => 'ObjectId',
124     );
125
126     # NewItem is necessary here because of RT::Report::Tickets and RT::Report::Tickets::Entry
127     my $item = $self->NewItem;
128     my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
129
130     $self->RT::SearchBuilder::Limit(
131         LEFTJOIN => $alias,
132         FIELD    => 'ObjectType',
133         VALUE    => $object_type,
134     );
135     $self->{'_sql_aliases'}{'transactions'} = $alias
136         unless $args{'New'};
137
138     return $alias;
139 }
140
141 sub _OrderByCF {
142     my $self = shift;
143     my ($row, $cfkey, $cf) = @_;
144
145     $cfkey .= ".ordering" if !blessed($cf) || ($cf->MaxValues||0) != 1;
146     my ($ocfvs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
147     # this is described in _LimitCustomField
148     $self->Limit(
149         ALIAS      => $CFs,
150         FIELD      => 'Name',
151         OPERATOR   => 'IS NOT',
152         VALUE      => 'NULL',
153         ENTRYAGGREGATOR => 'AND',
154         SUBCLAUSE  => ".ordering",
155     ) if $CFs;
156     my $CFvs = $self->Join(
157         TYPE   => 'LEFT',
158         ALIAS1 => $ocfvs,
159         FIELD1 => 'CustomField',
160         TABLE2 => 'CustomFieldValues',
161         FIELD2 => 'CustomField',
162     );
163     $self->Limit(
164         LEFTJOIN        => $CFvs,
165         FIELD           => 'Name',
166         QUOTEVALUE      => 0,
167         VALUE           => "$ocfvs.Content",
168         ENTRYAGGREGATOR => 'AND'
169     );
170
171     return { %$row, ALIAS => $CFvs,  FIELD => 'SortOrder' },
172            { %$row, ALIAS => $ocfvs, FIELD => 'Content' };
173 }
174
175 sub OrderByCols {
176     my $self = shift;
177     my @sort;
178     for my $s (@_) {
179         next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/;
180         $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION};
181         push @sort, $s;
182     }
183     return $self->SUPER::OrderByCols( @sort );
184 }
185
186 # If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
187 sub RowsPerPage {
188     my $self = shift;
189     return if @_ and defined $_[0] and $_[0] =~ /\D/;
190     return $self->SUPER::RowsPerPage(@_);
191 }
192
193 sub FirstRow {
194     my $self = shift;
195     return if @_ and defined $_[0] and $_[0] =~ /\D/;
196     return $self->SUPER::FirstRow(@_);
197 }
198
199 =head2 LimitToEnabled
200
201 Only find items that haven't been disabled
202
203 =cut
204
205 sub LimitToEnabled {
206     my $self = shift;
207
208     $self->{'handled_disabled_column'} = 1;
209     $self->Limit( FIELD => 'Disabled', VALUE => '0' );
210 }
211
212 =head2 LimitToDeleted
213
214 Only find items that have been deleted.
215
216 =cut
217
218 sub LimitToDeleted {
219     my $self = shift;
220
221     $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
222     $self->Limit( FIELD => 'Disabled', VALUE => '1' );
223 }
224
225 =head2 FindAllRows
226
227 Find all matching rows, regardless of whether they are disabled or not
228
229 =cut
230
231 sub FindAllRows {
232     shift->{'find_disabled_rows'} = 1;
233 }
234
235 =head2 LimitCustomField
236
237 Takes a paramhash of key/value pairs with the following keys:
238
239 =over 4
240
241 =item CUSTOMFIELD - CustomField id. Optional
242
243 =item OPERATOR - The usual Limit operators
244
245 =item VALUE - The value to compare against
246
247 =back
248
249 =cut
250
251 sub _SingularClass {
252     my $self = shift;
253     my $class = ref($self) || $self;
254     $class =~ s/s$// or die "Cannot deduce SingularClass for $class";
255     return $class;
256 }
257
258 =head2 RecordClass
259
260 Returns class name of records in this collection. This generic implementation
261 just strips trailing 's'.
262
263 =cut
264
265 sub RecordClass {
266     $_[0]->_SingularClass
267 }
268
269 =head2 RegisterCustomFieldJoin
270
271 Takes a pair of arguments, the first a class name and the second a callback
272 function.  The class will be used to call
273 L<RT::Record/CustomFieldLookupType>.  The callback will be called when
274 limiting a collection of the caller's class by a CF of the passed class's
275 lookup type.
276
277 The callback is passed a single argument, the current collection object (C<$self>).
278
279 An example from L<RT::Tickets>:
280
281     __PACKAGE__->RegisterCustomFieldJoin(
282         "RT::Transaction" => sub { $_[0]->JoinTransactions }
283     );
284
285 Returns true on success, undef on failure.
286
287 =cut
288
289 sub RegisterCustomFieldJoin {
290     my $class = shift;
291     my ($type, $callback) = @_;
292
293     $type = $type->CustomFieldLookupType if $type;
294
295     die "Unknown LookupType '$type'"
296         unless $type and grep { $_ eq $type } RT::CustomField->LookupTypes;
297
298     die "Custom field join callbacks must be CODE references"
299         unless ref($callback) eq 'CODE';
300
301     warn "Another custom field join callback is already registered for '$type'"
302         if $class->_JOINS_FOR_LOOKUP_TYPES->{$type};
303
304     # Stash the callback on ourselves
305     $class->_JOINS_FOR_LOOKUP_TYPES->{ $type } = $callback;
306
307     return 1;
308 }
309
310 =head2 _JoinForLookupType
311
312 Takes an L<RT::CustomField> LookupType and joins this collection as
313 appropriate to reach the object records to which LookupType applies.  The
314 object records will be of the class returned by
315 L<RT::CustomField/ObjectTypeFromLookupType>.
316
317 Returns the join alias suitable for further limiting against object
318 properties.
319
320 Returns undef on failure.
321
322 Used by L</_CustomFieldJoin>.
323
324 =cut
325
326 sub _JoinForLookupType {
327     my $self = shift;
328     my $type = shift or return;
329
330     # Convenience shortcut so that classes don't need to register a handler
331     # for their native lookup type
332     return "main" if $type eq $self->RecordClass->CustomFieldLookupType
333         and grep { $_ eq $type } RT::CustomField->LookupTypes;
334
335     my $JOINS = $self->_JOINS_FOR_LOOKUP_TYPES;
336     return $JOINS->{$type}->($self)
337         if ref $JOINS->{$type} eq 'CODE';
338
339     return;
340 }
341
342 sub _JOINS_FOR_LOOKUP_TYPES {
343     my $class = blessed($_[0]) || $_[0];
344     state %JOINS;
345     return $JOINS{$class} ||= {};
346 }
347
348 =head2 _CustomFieldJoin
349
350 Factor out the Join of custom fields so we can use it for sorting too
351
352 =cut
353
354 sub _CustomFieldJoin {
355     my ($self, $cfkey, $cf, $type) = @_;
356     $type ||= $self->RecordClass->CustomFieldLookupType;
357
358     # Perform one Join per CustomField
359     if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
360          $self->{_sql_cf_alias}{$cfkey} )
361     {
362         return ( $self->{_sql_object_cfv_alias}{$cfkey},
363                  $self->{_sql_cf_alias}{$cfkey} );
364     }
365
366     my $ObjectAlias = $self->_JoinForLookupType($type)
367         or die "We don't know how to join for LookupType $type";
368
369     my ($ocfvalias, $CFs);
370     if ( blessed($cf) ) {
371         $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
372             TYPE   => 'LEFT',
373             ALIAS1 => $ObjectAlias,
374             FIELD1 => 'id',
375             TABLE2 => 'ObjectCustomFieldValues',
376             FIELD2 => 'ObjectId',
377             $cf->SingleValue? (DISTINCT => 1) : (),
378         );
379         $self->Limit(
380             LEFTJOIN        => $ocfvalias,
381             FIELD           => 'CustomField',
382             VALUE           => $cf->id,
383             ENTRYAGGREGATOR => 'AND'
384         );
385     }
386     else {
387         ($ocfvalias, $CFs) = $self->_CustomFieldJoinByName( $ObjectAlias, $cf, $type );
388         $self->{_sql_cf_alias}{$cfkey} = $CFs;
389         $self->{_sql_object_cfv_alias}{$cfkey} = $ocfvalias;
390     }
391     $self->Limit(
392         LEFTJOIN        => $ocfvalias,
393         FIELD           => 'ObjectType',
394         VALUE           => RT::CustomField->ObjectTypeFromLookupType($type),
395         ENTRYAGGREGATOR => 'AND'
396     );
397     $self->Limit(
398         LEFTJOIN        => $ocfvalias,
399         FIELD           => 'Disabled',
400         OPERATOR        => '=',
401         VALUE           => '0',
402         ENTRYAGGREGATOR => 'AND'
403     );
404
405     return ($ocfvalias, $CFs);
406 }
407
408 sub _CustomFieldJoinByName {
409     my $self = shift;
410     my ($ObjectAlias, $cf, $type) = @_;
411     my $ocfalias = $self->Join(
412         TYPE       => 'LEFT',
413         EXPRESSION => q|'0'|,
414         TABLE2     => 'ObjectCustomFields',
415         FIELD2     => 'ObjectId',
416     );
417
418     my $CFs = $self->Join(
419         TYPE       => 'LEFT',
420         ALIAS1     => $ocfalias,
421         FIELD1     => 'CustomField',
422         TABLE2     => 'CustomFields',
423         FIELD2     => 'id',
424     );
425     $self->Limit(
426         LEFTJOIN        => $CFs,
427         ENTRYAGGREGATOR => 'AND',
428         FIELD           => 'LookupType',
429         VALUE           => $type,
430     );
431     $self->Limit(
432         LEFTJOIN        => $CFs,
433         ENTRYAGGREGATOR => 'AND',
434         FIELD           => 'Name',
435         CASESENSITIVE   => 0,
436         VALUE           => $cf,
437     );
438
439     my $ocfvalias = $self->Join(
440         TYPE   => 'LEFT',
441         ALIAS1 => $CFs,
442         FIELD1 => 'id',
443         TABLE2 => 'ObjectCustomFieldValues',
444         FIELD2 => 'CustomField',
445     );
446     $self->Limit(
447         LEFTJOIN        => $ocfvalias,
448         FIELD           => 'ObjectId',
449         VALUE           => "$ObjectAlias.id",
450         QUOTEVALUE      => 0,
451         ENTRYAGGREGATOR => 'AND',
452     );
453
454     return ($ocfvalias, $CFs, $ocfalias);
455 }
456
457 sub LimitCustomField {
458     my $self = shift;
459     return $self->_LimitCustomField( @_ );
460 }
461
462 use Regexp::Common qw(RE_net_IPv4);
463 use Regexp::Common::net::CIDR;
464
465 sub _LimitCustomField {
466     my $self = shift;
467     my %args = ( VALUE        => undef,
468                  CUSTOMFIELD  => undef,
469                  OPERATOR     => '=',
470                  KEY          => undef,
471                  PREPARSE     => 1,
472                  @_ );
473
474     my $op     = delete $args{OPERATOR};
475     my $value  = delete $args{VALUE};
476     my $ltype  = delete $args{LOOKUPTYPE} || $self->RecordClass->CustomFieldLookupType;
477     my $cf     = delete $args{CUSTOMFIELD};
478     my $column = delete $args{COLUMN};
479     my $cfkey  = delete $args{KEY};
480     if (blessed($cf) and $cf->id) {
481         $cfkey ||= $cf->id;
482     } elsif ($cf =~ /^\d+$/) {
483         # Intentionally load as the system user, so we can build better
484         # queries; this is necessary as we don't have a context object
485         # which might grant the user rights to see the CF.  This object
486         # is only used to inspect the properties of the CF itself.
487         my $obj = RT::CustomField->new( RT->SystemUser );
488         $obj->Load($cf);
489         if ($obj->id) {
490             $cf = $obj;
491             $cfkey ||= $cf->id;
492         } else {
493             $cfkey ||= "$ltype-$cf";
494         }
495     } else {
496         $cfkey ||= "$ltype-$cf";
497     }
498
499     $args{SUBCLAUSE} ||= "cf-$cfkey";
500
501
502     my $fix_op = sub {
503         return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
504
505         my %args = @_;
506         return %args unless $args{'FIELD'} eq 'LargeContent';
507
508         my $op = $args{'OPERATOR'};
509         if ( $op eq '=' ) {
510             $args{'OPERATOR'} = 'MATCHES';
511         }
512         elsif ( $op eq '!=' ) {
513             $args{'OPERATOR'} = 'NOT MATCHES';
514         }
515         elsif ( $op =~ /^[<>]=?$/ ) {
516             $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
517         }
518         return %args;
519     };
520
521     # Special Limit (we can exit early)
522     # IS NULL and IS NOT NULL checks
523     if ( $op =~ /^IS( NOT)?$/i ) {
524         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
525         $self->_OpenParen( $args{SUBCLAUSE} );
526         $self->Limit(
527             %args,
528             ALIAS    => $ocfvalias,
529             FIELD    => ($column || 'id'),
530             OPERATOR => $op,
531             VALUE    => $value,
532         );
533         # See below for an explanation of this limit
534         $self->Limit(
535             ALIAS      => $CFs,
536             FIELD      => 'Name',
537             OPERATOR   => 'IS NOT',
538             VALUE      => 'NULL',
539             ENTRYAGGREGATOR => 'AND',
540             SUBCLAUSE  => $args{SUBCLAUSE},
541         ) if $CFs;
542         $self->_CloseParen( $args{SUBCLAUSE} );
543         return;
544     }
545
546     ########## Content pre-parsing if we know things about the CF
547     if ( blessed($cf) and delete $args{PREPARSE} ) {
548         my $type = $cf->Type;
549         if ( $type eq 'IPAddress' ) {
550             my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
551             if ($parsed) {
552                 $value = $parsed;
553             } else {
554                 $RT::Logger->warn("$value is not a valid IPAddress");
555             }
556         } elsif ( $type eq 'IPAddressRange' ) {
557             my ( $start_ip, $end_ip ) =
558               RT::ObjectCustomFieldValue->ParseIPRange($value);
559             if ( $start_ip && $end_ip ) {
560                 if ( $op =~ /^<=?$/ ) {
561                     $value = $start_ip;
562                 } elsif ($op =~ /^>=?$/ ) {
563                     $value = $end_ip;
564                 } else {
565                     $value = join '-', $start_ip, $end_ip;
566                 }
567             } else {
568                 $RT::Logger->warn("$value is not a valid IPAddressRange");
569             }
570
571             # Recurse if they want a range comparison
572             if ( $op !~ /^[<>]=?$/ ) {
573                 my ($start_ip, $end_ip) = split /-/, $value;
574                 $self->_OpenParen( $args{SUBCLAUSE} );
575                 # Ideally we would limit >= 000.000.000.000 and <=
576                 # 255.255.255.255 so DB optimizers could use better
577                 # estimations and scan less rows, but this breaks with IPv6.
578                 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
579                     $self->_LimitCustomField(
580                         %args,
581                         OPERATOR    => '<=',
582                         VALUE       => $end_ip,
583                         LOOKUPTYPE  => $ltype,
584                         CUSTOMFIELD => $cf,
585                         COLUMN      => 'Content',
586                         PREPARSE    => 0,
587                     );
588                     $self->_LimitCustomField(
589                         %args,
590                         OPERATOR    => '>=',
591                         VALUE       => $start_ip,
592                         LOOKUPTYPE  => $ltype,
593                         CUSTOMFIELD => $cf,
594                         COLUMN      => 'LargeContent',
595                         ENTRYAGGREGATOR => 'AND',
596                         PREPARSE    => 0,
597                     );
598                 } else { # negative equation
599                     $self->_LimitCustomField(
600                         %args,
601                         OPERATOR    => '>',
602                         VALUE       => $end_ip,
603                         LOOKUPTYPE  => $ltype,
604                         CUSTOMFIELD => $cf,
605                         COLUMN      => 'Content',
606                         PREPARSE    => 0,
607                     );
608                     $self->_LimitCustomField(
609                         %args,
610                         OPERATOR    => '<',
611                         VALUE       => $start_ip,
612                         LOOKUPTYPE  => $ltype,
613                         CUSTOMFIELD => $cf,
614                         COLUMN      => 'LargeContent',
615                         ENTRYAGGREGATOR => 'OR',
616                         PREPARSE    => 0,
617                     );
618                 }
619                 $self->_CloseParen( $args{SUBCLAUSE} );
620                 return;
621             }
622         } elsif ( $type =~ /^Date(?:Time)?$/ ) {
623             my $date = RT::Date->new( $self->CurrentUser );
624             $date->Set( Format => 'unknown', Value => $value );
625             if ( $date->IsSet ) {
626                 if (
627                        $type eq 'Date'
628                            # Heuristics to determine if a date, and not
629                            # a datetime, was entered:
630                     || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
631                     || (   $value !~ /midnight|\d+:\d+:\d+/i
632                         && $date->Time( Timezone => 'user' ) eq '00:00:00' )
633                   )
634                 {
635                     $value = $date->Date( Timezone => 'user' );
636                 } else {
637                     $value = $date->DateTime;
638                 }
639             } else {
640                 $RT::Logger->warn("$value is not a valid date string");
641             }
642
643             # Recurse if day equality is being checked on a datetime
644             if ( $type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
645                 my $date = RT::Date->new( $self->CurrentUser );
646                 $date->Set( Format => 'unknown', Value => $value );
647                 my $daystart = $date->ISO;
648                 $date->AddDay;
649                 my $dayend = $date->ISO;
650
651                 $self->_OpenParen( $args{SUBCLAUSE} );
652                 $self->_LimitCustomField(
653                     %args,
654                     OPERATOR        => ">=",
655                     VALUE           => $daystart,
656                     LOOKUPTYPE      => $ltype,
657                     CUSTOMFIELD     => $cf,
658                     COLUMN          => 'Content',
659                     ENTRYAGGREGATOR => 'AND',
660                     PREPARSE        => 0,
661                 );
662
663                 $self->_LimitCustomField(
664                     %args,
665                     OPERATOR        => "<",
666                     VALUE           => $dayend,
667                     LOOKUPTYPE      => $ltype,
668                     CUSTOMFIELD     => $cf,
669                     COLUMN          => 'Content',
670                     ENTRYAGGREGATOR => 'AND',
671                     PREPARSE        => 0,
672                 );
673                 $self->_CloseParen( $args{SUBCLAUSE} );
674                 return;
675             }
676         }
677     }
678
679     ########## Limits
680
681     my $single_value = !blessed($cf) || $cf->SingleValue;
682     my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
683     my $value_is_long = (length( Encode::encode( "UTF-8", $value)) > 255) ? 1 : 0;
684
685     $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++
686         if not $single_value and $op =~ /^(!?=|(NOT )?LIKE)$/i;
687     my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
688
689     # A negative limit on a multi-value CF means _none_ of the values
690     # are the given value
691     if ( $negative_op and not $single_value ) {
692         # Reverse the limit we apply to the join, and check IS NULL
693         $op =~ s/!|NOT\s+//i;
694
695         # Ideally we would check both Content and LargeContent here, as
696         # the positive searches do below -- however, we cannot place
697         # complex limits inside LEFTJOINs due to searchbuilder
698         # limitations.  Guessing which to check based on the value's
699         # string length is sufficient for !=, but sadly insufficient for
700         # NOT LIKE checks, giving false positives.
701         $column ||= $value_is_long ? 'LargeContent' : 'Content';
702         $self->Limit( $fix_op->(
703             LEFTJOIN   => $ocfvalias,
704             ALIAS      => $ocfvalias,
705             FIELD      => $column,
706             OPERATOR   => $op,
707             VALUE      => $value,
708             CASESENSITIVE => 0,
709         ) );
710         $self->Limit(
711             %args,
712             ALIAS      => $ocfvalias,
713             FIELD      => 'id',
714             OPERATOR   => 'IS',
715             VALUE      => 'NULL',
716         );
717         return;
718     }
719
720     # If column is defined, then we just search it that, with no magic
721     if ( $column ) {
722         $self->_OpenParen( $args{SUBCLAUSE} );
723         $self->Limit( $fix_op->(
724             %args,
725             ALIAS      => $ocfvalias,
726             FIELD      => $column,
727             OPERATOR   => $op,
728             VALUE      => $value,
729             CASESENSITIVE => 0,
730         ) );
731         $self->Limit(
732             ALIAS           => $ocfvalias,
733             FIELD           => $column,
734             OPERATOR        => 'IS',
735             VALUE           => 'NULL',
736             ENTRYAGGREGATOR => 'OR',
737             SUBCLAUSE       => $args{SUBCLAUSE},
738         ) if $negative_op;
739         $self->_CloseParen( $args{SUBCLAUSE} );
740         return;
741     }
742
743     $self->_OpenParen( $args{SUBCLAUSE} ); # For negative_op "OR it is null" clause
744     $self->_OpenParen( $args{SUBCLAUSE} ); # NAME IS NOT NULL clause
745
746     $self->_OpenParen( $args{SUBCLAUSE} ); # Check Content / LargeContent
747     if ($value_is_long and $op eq "=") {
748         # Doesn't matter what Content contains, as it cannot match the
749         # too-long value; we just look in LargeContent, below.
750     } elsif ($value_is_long and $op =~ /^(!=|<>)$/) {
751         # If Content is non-null, that's a valid way to _not_ contain the too-long value.
752         $self->Limit(
753             %args,
754             ALIAS    => $ocfvalias,
755             FIELD    => 'Content',
756             OPERATOR => 'IS NOT',
757             VALUE    => 'NULL',
758         );
759     } else {
760         # Otherwise, go looking at the Content
761         $self->Limit(
762             %args,
763             ALIAS    => $ocfvalias,
764             FIELD    => 'Content',
765             OPERATOR => $op,
766             VALUE    => $value,
767             CASESENSITIVE => 0,
768         );
769     }
770
771     if (!$value_is_long and $op eq "=") {
772         # Doesn't matter what LargeContent contains, as it cannot match
773         # the short value.
774     } elsif (!$value_is_long and $op =~ /^(!=|<>)$/) {
775         # If LargeContent is non-null, that's a valid way to _not_
776         # contain the too-short value.
777         $self->Limit(
778             %args,
779             ALIAS    => $ocfvalias,
780             FIELD    => 'LargeContent',
781             OPERATOR => 'IS NOT',
782             VALUE    => 'NULL',
783             ENTRYAGGREGATOR => 'OR',
784         );
785     } else {
786         $self->_OpenParen( $args{SUBCLAUSE} ); # LargeContent check
787         $self->_OpenParen( $args{SUBCLAUSE} ); # Content is null?
788         $self->Limit(
789             ALIAS           => $ocfvalias,
790             FIELD           => 'Content',
791             OPERATOR        => '=',
792             VALUE           => '',
793             ENTRYAGGREGATOR => 'OR',
794             SUBCLAUSE       => $args{SUBCLAUSE},
795         );
796         $self->Limit(
797             ALIAS           => $ocfvalias,
798             FIELD           => 'Content',
799             OPERATOR        => 'IS',
800             VALUE           => 'NULL',
801             ENTRYAGGREGATOR => 'OR',
802             SUBCLAUSE       => $args{SUBCLAUSE},
803         );
804         $self->_CloseParen( $args{SUBCLAUSE} ); # Content is null?
805         $self->Limit( $fix_op->(
806             ALIAS           => $ocfvalias,
807             FIELD           => 'LargeContent',
808             OPERATOR        => $op,
809             VALUE           => $value,
810             ENTRYAGGREGATOR => 'AND',
811             SUBCLAUSE       => $args{SUBCLAUSE},
812             CASESENSITIVE => 0,
813         ) );
814         $self->_CloseParen( $args{SUBCLAUSE} ); # LargeContent check
815     }
816
817     $self->_CloseParen( $args{SUBCLAUSE} ); # Check Content/LargeContent
818
819     # XXX: if we join via CustomFields table then
820     # because of order of left joins we get NULLs in
821     # CF table and then get nulls for those records
822     # in OCFVs table what result in wrong results
823     # as decifer method now tries to load a CF then
824     # we fall into this situation only when there
825     # are more than one CF with the name in the DB.
826     # the same thing applies to order by call.
827     # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
828     # we want treat IS NULL as (not applies or has
829     # no value)
830     $self->Limit(
831         ALIAS           => $CFs,
832         FIELD           => 'Name',
833         OPERATOR        => 'IS NOT',
834         VALUE           => 'NULL',
835         ENTRYAGGREGATOR => 'AND',
836         SUBCLAUSE       => $args{SUBCLAUSE},
837     ) if $CFs;
838     $self->_CloseParen( $args{SUBCLAUSE} ); # Name IS NOT NULL clause
839
840     # If we were looking for != or NOT LIKE, we need to include the
841     # possibility that the row had no value.
842     $self->Limit(
843         ALIAS           => $ocfvalias,
844         FIELD           => 'id',
845         OPERATOR        => 'IS',
846         VALUE           => 'NULL',
847         ENTRYAGGREGATOR => 'OR',
848         SUBCLAUSE       => $args{SUBCLAUSE},
849     ) if $negative_op;
850     $self->_CloseParen( $args{SUBCLAUSE} ); # negative_op clause
851 }
852
853 =head2 Limit PARAMHASH
854
855 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
856 making sure that by default lots of things don't do extra work trying to 
857 match lower(colname) agaist lc($val);
858
859 We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
860 This ensures that we don't pass invalid SQL to the database or allow SQL
861 injection attacks when we pass through user specified values.
862
863 =cut
864
865 my %check_case_sensitivity = (
866     groups => { 'name' => 1, domain => 1 },
867     queues => { 'name' => 1 },
868     users => { 'name' => 1, emailaddress => 1 },
869     customfields => { 'name' => 1 },
870 );
871
872 my %deprecated = (
873     groups => {
874         type => 'Name',
875     },
876     principals => { objectid => 'id' },
877 );
878
879 sub Limit {
880     my $self = shift;
881     my %ARGS = (
882         OPERATOR => '=',
883         @_,
884     );
885
886     # We use the same regex here that DBIx::SearchBuilder uses to exclude
887     # values from quoting
888     if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
889         # Don't pass anything but NULL for IS and IS NOT
890         $ARGS{'VALUE'} = 'NULL';
891     }
892
893     if (($ARGS{FIELD}||'') =~ /\W/
894           or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
895                                   |(NOT\s*)?LIKE
896                                   |(NOT\s*)?(STARTS|ENDS)WITH
897                                   |(NOT\s*)?MATCHES
898                                   |IS(\s*NOT)?
899                                   |(NOT\s*)?IN
900                                   |\@\@)$/ix) {
901         $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
902         %ARGS = (
903             %ARGS,
904             FIELD    => 'id',
905             OPERATOR => '<',
906             VALUE    => '0',
907         );
908     }
909
910     my $table;
911     ($table) = $ARGS{'ALIAS'} && $ARGS{'ALIAS'} ne 'main'
912         ? ($ARGS{'ALIAS'} =~ /^(.*)_\d+$/)
913         : $self->Table
914     ;
915
916     if ( $table and $ARGS{FIELD} and my $instead = $deprecated{ lc $table }{ lc $ARGS{'FIELD'} } ) {
917         RT->Deprecated(
918             Message => "$table.$ARGS{'FIELD'} column is deprecated",
919             Instead => $instead, Remove => '4.4'
920         );
921     }
922
923     unless ( exists $ARGS{CASESENSITIVE} or (exists $ARGS{QUOTEVALUE} and not $ARGS{QUOTEVALUE}) ) {
924         if ( $ARGS{FIELD} and $ARGS{'OPERATOR'} !~ /IS/i
925             && $table && $check_case_sensitivity{ lc $table }{ lc $ARGS{'FIELD'} }
926         ) {
927             RT->Logger->warning(
928                 "Case sensitive search by $table.$ARGS{'FIELD'}"
929                 ." at ". (caller)[1] . " line ". (caller)[2]
930             );
931         }
932         $ARGS{'CASESENSITIVE'} = 1;
933     }
934
935     return $self->SUPER::Limit( %ARGS );
936 }
937
938 =head2 ItemsOrderBy
939
940 If it has a SortOrder attribute, sort the array by SortOrder.
941 Otherwise, if it has a "Name" attribute, sort alphabetically by Name
942 Otherwise, just give up and return it in the order it came from the
943 db.
944
945 =cut
946
947 sub ItemsOrderBy {
948     my $self = shift;
949     my $items = shift;
950   
951     if ($self->RecordClass->_Accessible('SortOrder','read')) {
952         $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ];
953     }
954     elsif ($self->RecordClass->_Accessible('Name','read')) {
955         $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ];
956     }
957
958     return $items;
959 }
960
961 =head2 ItemsArrayRef
962
963 Return this object's ItemsArray, in the order that ItemsOrderBy sorts
964 it.
965
966 =cut
967
968 sub ItemsArrayRef {
969     my $self = shift;
970     return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
971 }
972
973 # make sure that Disabled rows never get seen unless
974 # we're explicitly trying to see them.
975
976 sub _DoSearch {
977     my $self = shift;
978
979     if ( $self->{'with_disabled_column'}
980         && !$self->{'handled_disabled_column'}
981         && !$self->{'find_disabled_rows'}
982     ) {
983         $self->LimitToEnabled;
984     }
985     return $self->SUPER::_DoSearch(@_);
986 }
987 sub _DoCount {
988     my $self = shift;
989
990     if ( $self->{'with_disabled_column'}
991         && !$self->{'handled_disabled_column'}
992         && !$self->{'find_disabled_rows'}
993     ) {
994         $self->LimitToEnabled;
995     }
996     return $self->SUPER::_DoCount(@_);
997 }
998
999 =head2 ColumnMapClassName
1000
1001 ColumnMap needs a Collection name to load the correct list display.
1002 Depluralization is hard, so provide an easy way to correct the naive
1003 algorithm that this code uses.
1004
1005 =cut
1006
1007 sub ColumnMapClassName {
1008     my $self  = shift;
1009     my $Class = $self->_SingularClass;
1010        $Class =~ s/:/_/g;
1011     return $Class;
1012 }
1013
1014 =head2 NewItem
1015
1016 Returns a new item based on L</RecordClass> using the current user.
1017
1018 =cut
1019
1020 sub NewItem {
1021     my $self = shift;
1022     return $self->RecordClass->new($self->CurrentUser);
1023 }
1024
1025 =head2 NotSetDateToNullFunction
1026
1027 Takes a paramhash with an optional FIELD key whose value is the name of a date
1028 column.  If no FIELD is provided, a literal C<?> placeholder is used so the
1029 caller can fill in the field later.
1030
1031 Returns a SQL function which evaluates to C<NULL> if the FIELD is set to the
1032 Unix epoch; otherwise it evaluates to FIELD.  This is useful because RT
1033 currently stores unset dates as a Unix epoch timestamp instead of NULL, but
1034 NULLs are often more desireable.
1035
1036 =cut
1037
1038 sub NotSetDateToNullFunction {
1039     my $self = shift;
1040     my %args = ( FIELD => undef, @_ );
1041
1042     my $res = "CASE WHEN ? BETWEEN '1969-12-31 11:59:59' AND '1970-01-01 12:00:01' THEN NULL ELSE ? END";
1043     if ( $args{FIELD} ) {
1044         $res = $self->CombineFunctionWithField( %args, FUNCTION => $res );
1045     }
1046     return $res;
1047 }
1048
1049 RT::Base->_ImportOverlays();
1050
1051 1;