aae88b16d249ce91fcd982fd4a15286559f80221
[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 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     ########## Content pre-parsing if we know things about the CF
522     if ( blessed($cf) and delete $args{PREPARSE} ) {
523         my $type = $cf->Type;
524         if ( $type eq 'IPAddress' ) {
525             my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
526             if ($parsed) {
527                 $value = $parsed;
528             } else {
529                 $RT::Logger->warn("$value is not a valid IPAddress");
530             }
531         } elsif ( $type eq 'IPAddressRange' ) {
532             if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
533                 # convert incomplete 192.168/24 to 192.168.0.0/24 format
534                 $value =
535                   join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
536                   || $value;
537             }
538
539             my ( $start_ip, $end_ip ) =
540               RT::ObjectCustomFieldValue->ParseIPRange($value);
541             if ( $start_ip && $end_ip ) {
542                 if ( $op =~ /^<=?$/ ) {
543                     $value = $start_ip;
544                 } elsif ($op =~ /^>=?$/ ) {
545                     $value = $end_ip;
546                 } else {
547                     $value = join '-', $start_ip, $end_ip;
548                 }
549             } else {
550                 $RT::Logger->warn("$value is not a valid IPAddressRange");
551             }
552
553             # Recurse if they want a range comparison
554             if ( $op !~ /^[<>]=?$/ ) {
555                 my ($start_ip, $end_ip) = split /-/, $value;
556                 $self->_OpenParen( $args{SUBCLAUSE} );
557                 # Ideally we would limit >= 000.000.000.000 and <=
558                 # 255.255.255.255 so DB optimizers could use better
559                 # estimations and scan less rows, but this breaks with IPv6.
560                 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
561                     $self->_LimitCustomField(
562                         %args,
563                         OPERATOR    => '<=',
564                         VALUE       => $end_ip,
565                         LOOKUPTYPE  => $ltype,
566                         CUSTOMFIELD => $cf,
567                         COLUMN      => 'Content',
568                         PREPARSE    => 0,
569                     );
570                     $self->_LimitCustomField(
571                         %args,
572                         OPERATOR    => '>=',
573                         VALUE       => $start_ip,
574                         LOOKUPTYPE  => $ltype,
575                         CUSTOMFIELD => $cf,
576                         COLUMN      => 'LargeContent',
577                         ENTRYAGGREGATOR => 'AND',
578                         PREPARSE    => 0,
579                     );
580                 } else { # negative equation
581                     $self->_LimitCustomField(
582                         %args,
583                         OPERATOR    => '>',
584                         VALUE       => $end_ip,
585                         LOOKUPTYPE  => $ltype,
586                         CUSTOMFIELD => $cf,
587                         COLUMN      => 'Content',
588                         PREPARSE    => 0,
589                     );
590                     $self->_LimitCustomField(
591                         %args,
592                         OPERATOR    => '<',
593                         VALUE       => $start_ip,
594                         LOOKUPTYPE  => $ltype,
595                         CUSTOMFIELD => $cf,
596                         COLUMN      => 'LargeContent',
597                         ENTRYAGGREGATOR => 'OR',
598                         PREPARSE    => 0,
599                     );
600                 }
601                 $self->_CloseParen( $args{SUBCLAUSE} );
602                 return;
603             }
604         } elsif ( $type =~ /^Date(?:Time)?$/ ) {
605             my $date = RT::Date->new( $self->CurrentUser );
606             $date->Set( Format => 'unknown', Value => $value );
607             if ( $date->Unix ) {
608                 if (
609                        $type eq 'Date'
610                            # Heuristics to determine if a date, and not
611                            # a datetime, was entered:
612                     || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
613                     || (   $value !~ /midnight|\d+:\d+:\d+/i
614                         && $date->Time( Timezone => 'user' ) eq '00:00:00' )
615                   )
616                 {
617                     $value = $date->Date( Timezone => 'user' );
618                 } else {
619                     $value = $date->DateTime;
620                 }
621             } else {
622                 $RT::Logger->warn("$value is not a valid date string");
623             }
624
625             # Recurse if day equality is being checked on a datetime
626             if ( $type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
627                 my $date = RT::Date->new( $self->CurrentUser );
628                 $date->Set( Format => 'unknown', Value => $value );
629                 my $daystart = $date->ISO;
630                 $date->AddDay;
631                 my $dayend = $date->ISO;
632
633                 $self->_OpenParen( $args{SUBCLAUSE} );
634                 $self->_LimitCustomField(
635                     %args,
636                     OPERATOR        => ">=",
637                     VALUE           => $daystart,
638                     LOOKUPTYPE      => $ltype,
639                     CUSTOMFIELD     => $cf,
640                     COLUMN          => 'Content',
641                     ENTRYAGGREGATOR => 'AND',
642                     PREPARSE        => 0,
643                 );
644
645                 $self->_LimitCustomField(
646                     %args,
647                     OPERATOR        => "<",
648                     VALUE           => $dayend,
649                     LOOKUPTYPE      => $ltype,
650                     CUSTOMFIELD     => $cf,
651                     COLUMN          => 'Content',
652                     ENTRYAGGREGATOR => 'AND',
653                     PREPARSE        => 0,
654                 );
655                 $self->_CloseParen( $args{SUBCLAUSE} );
656                 return;
657             }
658         }
659     }
660
661     ########## Limits
662     # IS NULL and IS NOT NULL checks
663     if ( $op =~ /^IS( NOT)?$/i ) {
664         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
665         $self->_OpenParen( $args{SUBCLAUSE} );
666         $self->Limit(
667             %args,
668             ALIAS    => $ocfvalias,
669             FIELD    => ($column || 'id'),
670             OPERATOR => $op,
671             VALUE    => $value,
672         );
673         # See below for an explanation of this limit
674         $self->Limit(
675             ALIAS      => $CFs,
676             FIELD      => 'Name',
677             OPERATOR   => 'IS NOT',
678             VALUE      => 'NULL',
679             ENTRYAGGREGATOR => 'AND',
680             SUBCLAUSE  => $args{SUBCLAUSE},
681         ) if $CFs;
682         $self->_CloseParen( $args{SUBCLAUSE} );
683         return;
684     }
685
686     my $single_value = !blessed($cf) || $cf->SingleValue;
687     my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
688     my $value_is_long = (length( Encode::encode_utf8($value)) > 255) ? 1 : 0;
689
690     $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++
691         if not $single_value and $op =~ /^(!?=|(NOT )?LIKE)$/i;
692     my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
693
694     # A negative limit on a multi-value CF means _none_ of the values
695     # are the given value
696     if ( $negative_op and not $single_value ) {
697         # Reverse the limit we apply to the join, and check IS NULL
698         $op =~ s/!|NOT\s+//i;
699
700         # Ideally we would check both Content and LargeContent here, as
701         # the positive searches do below -- however, we cannot place
702         # complex limits inside LEFTJOINs due to searchbuilder
703         # limitations.  Guessing which to check based on the value's
704         # string length is sufficient for !=, but sadly insufficient for
705         # NOT LIKE checks, giving false positives.
706         $column ||= $value_is_long ? 'LargeContent' : 'Content';
707         $self->Limit( $fix_op->(
708             LEFTJOIN   => $ocfvalias,
709             ALIAS      => $ocfvalias,
710             FIELD      => $column,
711             OPERATOR   => $op,
712             VALUE      => $value,
713             CASESENSITIVE => 0,
714         ) );
715         $self->Limit(
716             %args,
717             ALIAS      => $ocfvalias,
718             FIELD      => 'id',
719             OPERATOR   => 'IS',
720             VALUE      => 'NULL',
721         );
722         return;
723     }
724
725     # If column is defined, then we just search it that, with no magic
726     if ( $column ) {
727         $self->_OpenParen( $args{SUBCLAUSE} );
728         $self->Limit( $fix_op->(
729             %args,
730             ALIAS      => $ocfvalias,
731             FIELD      => $column,
732             OPERATOR   => $op,
733             VALUE      => $value,
734             CASESENSITIVE => 0,
735         ) );
736         $self->Limit(
737             ALIAS           => $ocfvalias,
738             FIELD           => $column,
739             OPERATOR        => 'IS',
740             VALUE           => 'NULL',
741             ENTRYAGGREGATOR => 'OR',
742             SUBCLAUSE       => $args{SUBCLAUSE},
743         ) if $negative_op;
744         $self->_CloseParen( $args{SUBCLAUSE} );
745         return;
746     }
747
748     $self->_OpenParen( $args{SUBCLAUSE} ); # For negative_op "OR it is null" clause
749     $self->_OpenParen( $args{SUBCLAUSE} ); # NAME IS NOT NULL clause
750
751     $self->_OpenParen( $args{SUBCLAUSE} ); # Check Content / LargeContent
752     if ($value_is_long and $op eq "=") {
753         # Doesn't matter what Content contains, as it cannot match the
754         # too-long value; we just look in LargeContent, below.
755     } elsif ($value_is_long and $op =~ /^(!=|<>)$/) {
756         # If Content is non-null, that's a valid way to _not_ contain the too-long value.
757         $self->Limit(
758             %args,
759             ALIAS    => $ocfvalias,
760             FIELD    => 'Content',
761             OPERATOR => 'IS NOT',
762             VALUE    => 'NULL',
763         );
764     } else {
765         # Otherwise, go looking at the Content
766         $self->Limit(
767             %args,
768             ALIAS    => $ocfvalias,
769             FIELD    => 'Content',
770             OPERATOR => $op,
771             VALUE    => $value,
772             CASESENSITIVE => 0,
773         );
774     }
775
776     if (!$value_is_long and $op eq "=") {
777         # Doesn't matter what LargeContent contains, as it cannot match
778         # the short value.
779     } elsif (!$value_is_long and $op =~ /^(!=|<>)$/) {
780         # If LargeContent is non-null, that's a valid way to _not_
781         # contain the too-short value.
782         $self->Limit(
783             %args,
784             ALIAS    => $ocfvalias,
785             FIELD    => 'LargeContent',
786             OPERATOR => 'IS NOT',
787             VALUE    => 'NULL',
788             ENTRYAGGREGATOR => 'OR',
789         );
790     } else {
791         $self->_OpenParen( $args{SUBCLAUSE} ); # LargeContent check
792         $self->_OpenParen( $args{SUBCLAUSE} ); # Content is null?
793         $self->Limit(
794             ALIAS           => $ocfvalias,
795             FIELD           => 'Content',
796             OPERATOR        => '=',
797             VALUE           => '',
798             ENTRYAGGREGATOR => 'OR',
799             SUBCLAUSE       => $args{SUBCLAUSE},
800         );
801         $self->Limit(
802             ALIAS           => $ocfvalias,
803             FIELD           => 'Content',
804             OPERATOR        => 'IS',
805             VALUE           => 'NULL',
806             ENTRYAGGREGATOR => 'OR',
807             SUBCLAUSE       => $args{SUBCLAUSE},
808         );
809         $self->_CloseParen( $args{SUBCLAUSE} ); # Content is null?
810         $self->Limit( $fix_op->(
811             ALIAS           => $ocfvalias,
812             FIELD           => 'LargeContent',
813             OPERATOR        => $op,
814             VALUE           => $value,
815             ENTRYAGGREGATOR => 'AND',
816             SUBCLAUSE       => $args{SUBCLAUSE},
817             CASESENSITIVE => 0,
818         ) );
819         $self->_CloseParen( $args{SUBCLAUSE} ); # LargeContent check
820     }
821
822     $self->_CloseParen( $args{SUBCLAUSE} ); # Check Content/LargeContent
823
824     # XXX: if we join via CustomFields table then
825     # because of order of left joins we get NULLs in
826     # CF table and then get nulls for those records
827     # in OCFVs table what result in wrong results
828     # as decifer method now tries to load a CF then
829     # we fall into this situation only when there
830     # are more than one CF with the name in the DB.
831     # the same thing applies to order by call.
832     # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
833     # we want treat IS NULL as (not applies or has
834     # no value)
835     $self->Limit(
836         ALIAS           => $CFs,
837         FIELD           => 'Name',
838         OPERATOR        => 'IS NOT',
839         VALUE           => 'NULL',
840         ENTRYAGGREGATOR => 'AND',
841         SUBCLAUSE       => $args{SUBCLAUSE},
842     ) if $CFs;
843     $self->_CloseParen( $args{SUBCLAUSE} ); # Name IS NOT NULL clause
844
845     # If we were looking for != or NOT LIKE, we need to include the
846     # possibility that the row had no value.
847     $self->Limit(
848         ALIAS           => $ocfvalias,
849         FIELD           => 'id',
850         OPERATOR        => 'IS',
851         VALUE           => 'NULL',
852         ENTRYAGGREGATOR => 'OR',
853         SUBCLAUSE       => $args{SUBCLAUSE},
854     ) if $negative_op;
855     $self->_CloseParen( $args{SUBCLAUSE} ); # negative_op clause
856 }
857
858 =head2 Limit PARAMHASH
859
860 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
861 making sure that by default lots of things don't do extra work trying to 
862 match lower(colname) agaist lc($val);
863
864 We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
865 This ensures that we don't pass invalid SQL to the database or allow SQL
866 injection attacks when we pass through user specified values.
867
868 =cut
869
870 my %check_case_sensitivity = (
871     groups => { 'name' => 1, domain => 1 },
872     queues => { 'name' => 1 },
873     users => { 'name' => 1, emailaddress => 1 },
874     customfields => { 'name' => 1 },
875 );
876
877 my %deprecated = (
878     groups => {
879         type => 'Name',
880     },
881     principals => { objectid => 'id' },
882 );
883
884 sub Limit {
885     my $self = shift;
886     my %ARGS = (
887         OPERATOR => '=',
888         @_,
889     );
890
891     # We use the same regex here that DBIx::SearchBuilder uses to exclude
892     # values from quoting
893     if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
894         # Don't pass anything but NULL for IS and IS NOT
895         $ARGS{'VALUE'} = 'NULL';
896     }
897
898     if (($ARGS{FIELD}||'') =~ /\W/
899           or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
900                                   |(NOT\s*)?LIKE
901                                   |(NOT\s*)?(STARTS|ENDS)WITH
902                                   |(NOT\s*)?MATCHES
903                                   |IS(\s*NOT)?
904                                   |(NOT\s*)?IN
905                                   |\@\@)$/ix) {
906         $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
907         %ARGS = (
908             %ARGS,
909             FIELD    => 'id',
910             OPERATOR => '<',
911             VALUE    => '0',
912         );
913     }
914
915     my $table;
916     ($table) = $ARGS{'ALIAS'} && $ARGS{'ALIAS'} ne 'main'
917         ? ($ARGS{'ALIAS'} =~ /^(.*)_\d+$/)
918         : $self->Table
919     ;
920
921     if ( $table and $ARGS{FIELD} and my $instead = $deprecated{ lc $table }{ lc $ARGS{'FIELD'} } ) {
922         RT->Deprecated(
923             Message => "$table.$ARGS{'FIELD'} column is deprecated",
924             Instead => $instead, Remove => '4.4'
925         );
926     }
927
928     unless ( exists $ARGS{CASESENSITIVE} or (exists $ARGS{QUOTEVALUE} and not $ARGS{QUOTEVALUE}) ) {
929         if ( $ARGS{FIELD} and $ARGS{'OPERATOR'} !~ /IS/i
930             && $table && $check_case_sensitivity{ lc $table }{ lc $ARGS{'FIELD'} }
931         ) {
932             RT->Logger->warning(
933                 "Case sensitive search by $table.$ARGS{'FIELD'}"
934                 ." at ". (caller)[1] . " line ". (caller)[2]
935             );
936         }
937         $ARGS{'CASESENSITIVE'} = 1;
938     }
939
940     return $self->SUPER::Limit( %ARGS );
941 }
942
943 =head2 ItemsOrderBy
944
945 If it has a SortOrder attribute, sort the array by SortOrder.
946 Otherwise, if it has a "Name" attribute, sort alphabetically by Name
947 Otherwise, just give up and return it in the order it came from the
948 db.
949
950 =cut
951
952 sub ItemsOrderBy {
953     my $self = shift;
954     my $items = shift;
955   
956     if ($self->RecordClass->_Accessible('SortOrder','read')) {
957         $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ];
958     }
959     elsif ($self->RecordClass->_Accessible('Name','read')) {
960         $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ];
961     }
962
963     return $items;
964 }
965
966 =head2 ItemsArrayRef
967
968 Return this object's ItemsArray, in the order that ItemsOrderBy sorts
969 it.
970
971 =cut
972
973 sub ItemsArrayRef {
974     my $self = shift;
975     return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
976 }
977
978 # make sure that Disabled rows never get seen unless
979 # we're explicitly trying to see them.
980
981 sub _DoSearch {
982     my $self = shift;
983
984     if ( $self->{'with_disabled_column'}
985         && !$self->{'handled_disabled_column'}
986         && !$self->{'find_disabled_rows'}
987     ) {
988         $self->LimitToEnabled;
989     }
990     return $self->SUPER::_DoSearch(@_);
991 }
992 sub _DoCount {
993     my $self = shift;
994
995     if ( $self->{'with_disabled_column'}
996         && !$self->{'handled_disabled_column'}
997         && !$self->{'find_disabled_rows'}
998     ) {
999         $self->LimitToEnabled;
1000     }
1001     return $self->SUPER::_DoCount(@_);
1002 }
1003
1004 =head2 ColumnMapClassName
1005
1006 ColumnMap needs a Collection name to load the correct list display.
1007 Depluralization is hard, so provide an easy way to correct the naive
1008 algorithm that this code uses.
1009
1010 =cut
1011
1012 sub ColumnMapClassName {
1013     my $self  = shift;
1014     my $Class = $self->_SingularClass;
1015        $Class =~ s/:/_/g;
1016     return $Class;
1017 }
1018
1019 =head2 NewItem
1020
1021 Returns a new item based on L</RecordClass> using the current user.
1022
1023 =cut
1024
1025 sub NewItem {
1026     my $self = shift;
1027     return $self->RecordClass->new($self->CurrentUser);
1028 }
1029
1030 =head2 NotSetDateToNullFunction
1031
1032 Takes a paramhash with an optional FIELD key whose value is the name of a date
1033 column.  If no FIELD is provided, a literal C<?> placeholder is used so the
1034 caller can fill in the field later.
1035
1036 Returns a SQL function which evaluates to C<NULL> if the FIELD is set to the
1037 Unix epoch; otherwise it evaluates to FIELD.  This is useful because RT
1038 currently stores unset dates as a Unix epoch timestamp instead of NULL, but
1039 NULLs are often more desireable.
1040
1041 =cut
1042
1043 sub NotSetDateToNullFunction {
1044     my $self = shift;
1045     my %args = ( FIELD => undef, @_ );
1046
1047     my $res = "CASE WHEN ? BETWEEN '1969-12-31 11:59:59' AND '1970-01-01 12:00:01' THEN NULL ELSE ? END";
1048     if ( $args{FIELD} ) {
1049         $res = $self->CombineFunctionWithField( %args, FUNCTION => $res );
1050     }
1051     return $res;
1052 }
1053
1054 RT::Base->_ImportOverlays();
1055
1056 1;