+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
+
+=cut
+
+sub _CustomFieldDecipher {
+ my ($self, $string) = @_;
+
+ my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
+ $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
+
+ my $cf;
+ if ( $queue ) {
+ my $q = RT::Queue->new( $self->CurrentUser );
+ $q->Load( $queue );
+
+ if ( $q->id ) {
+ # $queue = $q->Name; # should we normalize the queue?
+ $cf = $q->CustomField( $field );
+ }
+ else {
+ $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
+ $queue = 0;
+ }
+ }
+ elsif ( $field =~ /\D/ ) {
+ $queue = '';
+ my $cfs = RT::CustomFields->new( $self->CurrentUser );
+ $cfs->Limit( FIELD => 'Name', VALUE => $field );
+ $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
+
+ # if there is more then one field the current user can
+ # see with the same name then we shouldn't return cf object
+ # as we don't know which one to use
+ $cf = $cfs->First;
+ if ( $cf ) {
+ $cf = undef if $cfs->Next;
+ }
+ }
+ else {
+ $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->Load( $field );
+ }
+
+ return ($queue, $field, $cf, $column);
+}
+
+=head2 _CustomFieldJoin
+
+Factor out the Join of custom fields so we can use it for sorting too
+
+=cut
+
+sub _CustomFieldJoin {
+ my ($self, $cfkey, $cfid, $field) = @_;
+ # Perform one Join per CustomField
+ if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
+ $self->{_sql_cf_alias}{$cfkey} )
+ {
+ return ( $self->{_sql_object_cfv_alias}{$cfkey},
+ $self->{_sql_cf_alias}{$cfkey} );
+ }
+
+ my ($TicketCFs, $CFs);
+ if ( $cfid ) {
+ $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'ObjectId',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $TicketCFs,
+ FIELD => 'CustomField',
+ VALUE => $cfid,
+ ENTRYAGGREGATOR => 'AND'
+ );
+ }
+ else {
+ my $ocfalias = $self->Join(
+ TYPE => 'LEFT',
+ FIELD1 => 'Queue',
+ TABLE2 => 'ObjectCustomFields',
+ FIELD2 => 'ObjectId',
+ );
+
+ $self->SUPER::Limit(
+ LEFTJOIN => $ocfalias,
+ ENTRYAGGREGATOR => 'OR',
+ FIELD => 'ObjectId',
+ VALUE => '0',
+ );
+
+ $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $ocfalias,
+ FIELD1 => 'CustomField',
+ TABLE2 => 'CustomFields',
+ FIELD2 => 'id',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $CFs,
+ ENTRYAGGREGATOR => 'AND',
+ FIELD => 'LookupType',
+ VALUE => 'RT::Queue-RT::Ticket',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $CFs,
+ ENTRYAGGREGATOR => 'AND',
+ FIELD => 'Name',
+ VALUE => $field,
+ );
+
+ $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $CFs,
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'CustomField',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $TicketCFs,
+ FIELD => 'ObjectId',
+ VALUE => 'main.id',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ }
+ $self->SUPER::Limit(
+ LEFTJOIN => $TicketCFs,
+ FIELD => 'ObjectType',
+ VALUE => 'RT::Ticket',
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $TicketCFs,
+ FIELD => 'Disabled',
+ OPERATOR => '=',
+ VALUE => '0',
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ return ($TicketCFs, $CFs);
+}
+
+=head2 _CustomFieldLimit
+
+Limit based on CustomFields
+
+Meta Data:
+ none
+
+=cut
+
+sub _CustomFieldLimit {
+ my ( $self, $_field, $op, $value, %rest ) = @_;
+
+ my $field = $rest{'SUBKEY'} || die "No field specified";
+
+ # For our sanity, we can only limit on one queue at a time
+
+ my ($queue, $cfid, $cf, $column);
+ ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
+ $cfid = $cf ? $cf->id : 0 ;
+
+# If we're trying to find custom fields that don't match something, we
+# want tickets where the custom field has no value at all. Note that
+# we explicitly don't include the "IS NULL" case, since we would
+# otherwise end up with a redundant clause.
+
+ my ($negative_op, $null_op, $inv_op, $range_op)
+ = $self->ClassifySQLOperation( $op );
+
+ my $fix_op = sub {
+ my $op = shift;
+ return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
+ return 'MATCHES' if $op eq '=';
+ return 'NOT MATCHES' if $op eq '!=';
+ return $op;
+ };
+
+ my $single_value = !$cf || !$cfid || $cf->SingleValue;
+
+ my $cfkey = $cfid ? $cfid : "$queue.$field";
+
+ if ( $null_op && !$column ) {
+ # IS[ NOT] NULL without column is the same as has[ no] any CF value,
+ # we can reuse our default joins for this operation
+ # with column specified we have different situation
+ my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'id',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+ $self->_SQLLimit(
+ ALIAS => $CFs,
+ FIELD => 'Name',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ ) if $CFs;
+ $self->_CloseParen;
+ }
+ elsif ( !$negative_op || $single_value ) {
+ $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
+ my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+ $self->_OpenParen;
+
+ $self->_OpenParen;
+
+ $self->_OpenParen;
+ # if column is defined then deal only with it
+ # otherwise search in Content and in LargeContent
+ if ( $column ) {
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => $column,
+ OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
+ VALUE => $value,
+ %rest
+ );
+ }
+ elsif ( $cf->Type eq 'Date' ) {
+ $self->_DateFieldLimit(
+ 'Content',
+ $op,
+ $value,
+ ALIAS => $TicketCFs,
+ %rest
+ );
+ }
+ elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+ unless ( length( Encode::encode_utf8($value) ) > 255 ) {
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+ } else {
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $fix_op->($op),
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ }
+ }
+ else {
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+
+ $self->_OpenParen;
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $fix_op->($op),
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ $self->_CloseParen;
+ }
+ $self->_CloseParen;
+
+ # XXX: if we join via CustomFields table then
+ # because of order of left joins we get NULLs in
+ # CF table and then get nulls for those records
+ # in OCFVs table what result in wrong results
+ # as decifer method now tries to load a CF then
+ # we fall into this situation only when there
+ # are more than one CF with the name in the DB.
+ # the same thing applies to order by call.
+ # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
+ # we want treat IS NULL as (not applies or has
+ # no value)
+ $self->_SQLLimit(
+ ALIAS => $CFs,
+ FIELD => 'Name',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,