+
+sub _IdLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ if ( $value eq '__Bookmarked__' ) {
+ return $sb->_BookmarkLimit( $field, $op, $value, @rest );
+ } else {
+ return $sb->_IntLimit( $field, $op, $value, @rest );
+ }
+}
+
+sub _BookmarkLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ die "Invalid operator $op for __Bookmarked__ search on $field"
+ unless $op =~ /^(=|!=)$/;
+
+ my @bookmarks = $sb->CurrentUser->UserObj->Bookmarks;
+
+ return $sb->Limit(
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => 0,
+ @rest,
+ ) unless @bookmarks;
+
+ # as bookmarked tickets can be merged we have to use a join
+ # but it should be pretty lightweight
+ my $tickets_alias = $sb->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Tickets',
+ FIELD2 => 'EffectiveId',
+ );
+
+ $op = $op eq '='? 'IN': 'NOT IN';
+ $sb->Limit(
+ ALIAS => $tickets_alias,
+ FIELD => 'id',
+ OPERATOR => $op,
+ VALUE => [ @bookmarks ],
+ @rest,
+ );
+}
+
+=head2 _EnumLimit
+
+Handle Fields which are limited to certain values, and potentially
+need to be looked up from another class.
+
+This subroutine actually handles two different kinds of fields. For
+some the user is responsible for limiting the values. (i.e. Status,
+Type).
+
+For others, the value specified by the user will be looked by via
+specified class.
+
+Meta Data:
+ name of class to lookup in (Optional)
+
+=cut
+
+sub _EnumLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ # SQL::Statement changes != to <>. (Can we remove this now?)
+ $op = "!=" if $op eq "<>";
+
+ die "Invalid Operation: $op for $field"
+ unless $op eq "="
+ or $op eq "!=";
+
+ my $meta = $FIELD_METADATA{$field};
+ if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
+ my $class = "RT::" . $meta->[1];
+ my $o = $class->new( $sb->CurrentUser );
+ $o->Load($value);
+ $value = $o->Id || 0;
+ } elsif ( $field eq "Type" ) {
+ $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
+ }
+ $sb->Limit(
+ FIELD => $field,
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
+}
+
+=head2 _IntLimit
+
+Handle fields where the values are limited to integers. (For example,
+Priority, TimeWorked.)
+
+Meta Data:
+ None
+
+=cut
+
+sub _IntLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
+
+ # We want to support <id LIKE '1%'> for ticket autocomplete,
+ # but we need to explicitly typecast on Postgres
+ if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
+ return $sb->Limit(
+ FUNCTION => "CAST(main.$field AS TEXT)",
+ OPERATOR => $op,
+ VALUE => $value,
+ @rest,
+ );
+ }
+
+ $sb->Limit(
+ FIELD => $field,
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
+}
+
+=head2 _LinkLimit
+
+Handle fields which deal with links between tickets. (MemberOf, DependsOn)
+
+Meta Data:
+ 1: Direction (From, To)
+ 2: Link Type (MemberOf, DependsOn, RefersTo)
+
+=cut
+
+sub _LinkLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ my $meta = $FIELD_METADATA{$field};
+ die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
+
+ my $is_negative = 0;
+ if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+ $is_negative = 1;
+ }
+ my $is_null = 0;
+ $is_null = 1 if !$value || $value =~ /^null$/io;
+
+ my $direction = $meta->[1] || '';
+ my ($matchfield, $linkfield) = ('', '');
+ if ( $direction eq 'To' ) {
+ ($matchfield, $linkfield) = ("Target", "Base");
+ }
+ elsif ( $direction eq 'From' ) {
+ ($matchfield, $linkfield) = ("Base", "Target");
+ }
+ elsif ( $direction ) {
+ die "Invalid link direction '$direction' for $field\n";
+ } else {
+ $sb->_OpenParen;
+ $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
+ $sb->_LinkLimit(
+ 'LinkedFrom', $op, $value, @rest,
+ ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
+ );
+ $sb->_CloseParen;
+ return;
+ }
+
+ my $is_local = 1;
+ if ( $is_null ) {
+ $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
+ }
+ elsif ( $value =~ /\D/ ) {
+ $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+ $is_local = 0;
+ }
+ $matchfield = "Local$matchfield" if $is_local;
+
+#For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
+# SELECT main.* FROM Tickets main
+# LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
+# AND(main.id = Links_1.LocalTarget))
+# WHERE Links_1.LocalBase IS NULL;
+
+ if ( $is_null ) {
+ my $linkalias = $sb->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => 'Local' . $linkfield
+ );
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->Limit(
+ @rest,
+ ALIAS => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => $op,
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+ else {
+ my $linkalias = $sb->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => 'Local' . $linkfield
+ );
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => '=',
+ VALUE => $value,
+ );
+ $sb->Limit(
+ @rest,
+ ALIAS => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => $is_negative? 'IS': 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+}
+
+=head2 _DateLimit
+
+Handle date fields. (Created, LastTold..)
+
+Meta Data:
+ 1: type of link. (Probably not necessary.)
+
+=cut
+
+sub _DateLimit {
+ my ( $sb, $field, $op, $value, %rest ) = @_;
+
+ die "Invalid Date Op: $op"
+ unless $op =~ /^(=|>|<|>=|<=|IS(\s+NOT)?)$/i;
+
+ my $meta = $FIELD_METADATA{$field};
+ die "Incorrect Meta Data for $field"
+ unless ( defined $meta->[1] );
+
+ $sb->_DateFieldLimit( $meta->[1], $op, $value, %rest );
+}
+
+# Factor this out for use by custom fields
+
+sub _DateFieldLimit {
+ my ( $sb, $field, $op, $value, %rest ) = @_;
+
+ if ( $op =~ /^(IS(\s+NOT)?)$/i) {
+ return $sb->Limit(
+ FUNCTION => $sb->NotSetDateToNullFunction,
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => "NULL",
+ %rest,
+ );
+ }
+
+ if ( my $subkey = $rest{SUBKEY} ) {
+ if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+ for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
+ # Use a case-insensitive regex for better matching across
+ # locales since we don't have fc() and lc() is worse. Really
+ # we should be doing Unicode normalization too, but we don't do
+ # that elsewhere in RT.
+ #
+ # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+ next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value
+ or $sb->CurrentUser->loc($RT::Date::DAYS_OF_WEEK[ $i ]) =~ /^\Q$value\E$/i;
+
+ $value = $i; last;
+ }
+ return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+ if $value =~ /[^0-9]/;
+ }
+ elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+ for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
+ # Use a case-insensitive regex for better matching across
+ # locales since we don't have fc() and lc() is worse. Really
+ # we should be doing Unicode normalization too, but we don't do
+ # that elsewhere in RT.
+ #
+ # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+ next unless lc $RT::Date::MONTHS[ $i ] eq lc $value
+ or $sb->CurrentUser->loc($RT::Date::MONTHS[ $i ]) =~ /^\Q$value\E$/i;
+
+ $value = $i + 1; last;
+ }
+ return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+ if $value =~ /[^0-9]/;
+ }
+
+ my $tz;
+ if ( RT->Config->Get('ChartsTimezonesInDB') ) {
+ my $to = $sb->CurrentUser->UserObj->Timezone
+ || RT->Config->Get('Timezone');
+ $tz = { From => 'UTC', To => $to }
+ if $to && lc $to ne 'utc';
+ }
+
+ # $subkey is validated by DateTimeFunction
+ my $function = $RT::Handle->DateTimeFunction(
+ Type => $subkey,
+ Field => $sb->NotSetDateToNullFunction,
+ Timezone => $tz,
+ );
+
+ return $sb->Limit(
+ FUNCTION => $function,
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest,
+ );
+ }
+
+ my $date = RT::Date->new( $sb->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+
+ if ( $op eq "=" ) {
+
+ # if we're specifying =, that means we want everything on a
+ # particular single day. in the database, we need to check for >
+ # and < the edges of that day.
+ #
+ # Except if the value is 'this month' or 'last month', check
+ # > and < the edges of the month.
+
+ my ($daystart, $dayend);
+ if ( lc($value) eq 'this month' ) {
+ $date->SetToNow;
+ $date->SetToStart('month', Timezone => 'server');
+ $daystart = $date->ISO;
+ $date->AddMonth(Timezone => 'server');
+ $dayend = $date->ISO;
+ }
+ elsif ( lc($value) eq 'last month' ) {
+ $date->SetToNow;
+ $date->SetToStart('month', Timezone => 'server');
+ $dayend = $date->ISO;
+ $date->AddDays(-1);
+ $date->SetToStart('month', Timezone => 'server');
+ $daystart = $date->ISO;
+ }
+ else {
+ $date->SetToMidnight( Timezone => 'server' );
+ $daystart = $date->ISO;
+ $date->AddDay;
+ $dayend = $date->ISO;
+ }
+
+ $sb->_OpenParen;
+
+ $sb->Limit(
+ FIELD => $field,
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ %rest,
+ );
+
+ $sb->Limit(
+ FIELD => $field,
+ OPERATOR => "<",
+ VALUE => $dayend,
+ %rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ $sb->_CloseParen;
+
+ }
+ else {
+ $sb->Limit(
+ FUNCTION => $sb->NotSetDateToNullFunction,
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $date->ISO,
+ %rest,
+ );
+ }
+}
+
+=head2 _StringLimit
+
+Handle simple fields which are just strings. (Subject,Type)
+
+Meta Data:
+ None
+
+=cut
+
+sub _StringLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ # FIXME:
+ # Valid Operators:
+ # =, !=, LIKE, NOT LIKE
+ if ( RT->Config->Get('DatabaseType') eq 'Oracle'
+ && (!defined $value || !length $value)
+ && lc($op) ne 'is' && lc($op) ne 'is not'
+ ) {
+ if ($op eq '!=' || $op =~ /^NOT\s/i) {
+ $op = 'IS NOT';
+ } else {
+ $op = 'IS';
+ }
+ $value = 'NULL';
+ }
+
+ if ($field eq "Status") {
+ $value = lc $value;
+ }
+
+ $sb->Limit(
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ @rest,
+ );
+}
+
+=head2 _TransDateLimit
+
+Handle fields limiting based on Transaction Date.
+
+The inpupt value must be in a format parseable by Time::ParseDate
+
+Meta Data:
+ None
+
+=cut
+
+# This routine should really be factored into translimit.
+sub _TransDateLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ # See the comments for TransLimit, they apply here too
+
+ my $txn_alias = $sb->JoinTransactions;
+
+ my $date = RT::Date->new( $sb->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+
+ $sb->_OpenParen;
+ if ( $op eq "=" ) {
+
+ # if we're specifying =, that means we want everything on a
+ # particular single day. in the database, we need to check for >
+ # and < the edges of that day.
+
+ $date->SetToMidnight( Timezone => 'server' );
+ my $daystart = $date->ISO;
+ $date->AddDay;
+ my $dayend = $date->ISO;
+
+ $sb->Limit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ @rest
+ );
+ $sb->Limit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => "<=",
+ VALUE => $dayend,
+ @rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ }
+
+ # not searching for a single day
+ else {
+
+ #Search for the right field
+ $sb->Limit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => $op,
+ VALUE => $date->ISO,
+ @rest
+ );
+ }
+
+ $sb->_CloseParen;
+}
+
+sub _TransCreatorLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+ $op = "!=" if $op eq "<>";
+ die "Invalid Operation: $op for $field" unless $op eq "=" or $op eq "!=";
+
+ # See the comments for TransLimit, they apply here too
+ my $txn_alias = $sb->JoinTransactions;
+ if ( defined $value && $value !~ /^\d+$/ ) {
+ my $u = RT::User->new( $sb->CurrentUser );
+ $u->Load($value);
+ $value = $u->id || 0;
+ }
+ $sb->Limit( ALIAS => $txn_alias, FIELD => 'Creator', OPERATOR => $op, VALUE => $value, @rest );
+}
+
+=head2 _TransLimit
+
+Limit based on the ContentType or the Filename of a transaction.
+
+=cut
+
+sub _TransLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ my $txn_alias = $self->JoinTransactions;
+ unless ( defined $self->{_sql_trattachalias} ) {
+ $self->{_sql_trattachalias} = $self->Join(
+ TYPE => 'LEFT', # not all txns have an attachment
+ ALIAS1 => $txn_alias,
+ FIELD1 => 'id',
+ TABLE2 => 'Attachments',
+ FIELD2 => 'TransactionId',
+ );
+ }
+
+ $self->Limit(
+ %rest,
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ );
+}
+
+=head2 _TransContentLimit
+
+Limit based on the Content of a transaction.
+
+=cut
+
+sub _TransContentLimit {
+
+ # Content search
+
+ # If only this was this simple. We've got to do something
+ # complicated here:
+
+ #Basically, we want to make sure that the limits apply to
+ #the same attachment, rather than just another attachment
+ #for the same ticket, no matter how many clauses we lump
+ #on.
+
+ # In the SQL, we might have
+ # (( Content = foo ) or ( Content = bar AND Content = baz ))
+ # The AND group should share the same Alias.
+
+ # Actually, maybe it doesn't matter. We use the same alias and it
+ # works itself out? (er.. different.)
+
+ # Steal more from _ProcessRestrictions
+
+ # FIXME: Maybe look at the previous FooLimit call, and if it was a
+ # TransLimit and EntryAggregator == AND, reuse the Aliases?
+
+ # Or better - store the aliases on a per subclause basis - since
+ # those are going to be the things we want to relate to each other,
+ # anyway.
+
+ # maybe we should not allow certain kinds of aggregation of these
+ # clauses and do a psuedo regex instead? - the problem is getting
+ # them all into the same subclause when you have (A op B op C) - the
+ # way they get parsed in the tree they're in different subclauses.
+
+ my ( $self, $field, $op, $value, %rest ) = @_;
+ $field = 'Content' if $field =~ /\W/;
+
+ my $config = RT->Config->Get('FullTextSearch') || {};
+ unless ( $config->{'Enable'} ) {
+ $self->Limit( %rest, FIELD => 'id', VALUE => 0 );
+ return;
+ }
+
+ my $txn_alias = $self->JoinTransactions;
+ unless ( defined $self->{_sql_trattachalias} ) {
+ $self->{_sql_trattachalias} = $self->Join(
+ TYPE => 'LEFT', # not all txns have an attachment
+ ALIAS1 => $txn_alias,
+ FIELD1 => 'id',
+ TABLE2 => 'Attachments',
+ FIELD2 => 'TransactionId',
+ );
+ }
+
+ $self->_OpenParen;
+ if ( $config->{'Indexed'} ) {
+ my $db_type = RT->Config->Get('DatabaseType');
+
+ my $alias;
+ if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
+ $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $self->{'_sql_trattachalias'},
+ FIELD1 => 'id',
+ TABLE2 => $config->{'Table'},
+ FIELD2 => 'id',
+ );
+ } else {
+ $alias = $self->{'_sql_trattachalias'};
+ }
+
+ #XXX: handle negative searches
+ my $index = $config->{'Column'};
+ if ( $db_type eq 'Oracle' ) {
+ my $dbh = $RT::Handle->dbh;
+ my $alias = $self->{_sql_trattachalias};
+ $self->Limit(
+ %rest,
+ FUNCTION => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
+ OPERATOR => '>',
+ VALUE => 0,
+ QUOTEVALUE => 0,
+ CASESENSITIVE => 1,
+ );
+ # this is required to trick DBIx::SB's LEFT JOINS optimizer
+ # into deciding that join is redundant as it is
+ $self->Limit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => 'Content',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ );
+ }
+ elsif ( $db_type eq 'Pg' ) {
+ my $dbh = $RT::Handle->dbh;
+ $self->Limit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => $index,
+ OPERATOR => '@@',
+ VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
+ QUOTEVALUE => 0,
+ );
+ }
+ elsif ( $db_type eq 'mysql' and not $config->{Sphinx}) {
+ my $dbh = $RT::Handle->dbh;
+ $self->Limit(
+ %rest,
+ FUNCTION => "MATCH($alias.Content)",
+ OPERATOR => 'AGAINST',
+ VALUE => "(". $dbh->quote($value) ." IN BOOLEAN MODE)",
+ QUOTEVALUE => 0,
+ );
+ # As with Oracle, above, this forces the LEFT JOINs into
+ # JOINS, which allows the FULLTEXT index to be used.
+ # Orthogonally, the IS NOT NULL clause also helps the
+ # optimizer decide to use the index.
+ $self->Limit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $alias,
+ FIELD => "Content",
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+ elsif ( $db_type eq 'mysql' ) {
+ # XXX: We could theoretically skip the join to Attachments,
+ # and have Sphinx simply index and group by the TicketId,
+ # and join Ticket.id to that attribute, which would be much
+ # more efficient -- however, this is only a possibility if
+ # there are no other transaction limits.
+
+ # This is a special character. Note that \ does not escape
+ # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
+ # 'foo\\;bar' is not a vulnerability, and is still parsed as
+ # "foo, \, ;, then bar". Happily, the default mode is
+ # "all", meaning that boolean operators are not special.
+ $value =~ s/;/\\;/g;
+
+ my $max = $config->{'MaxMatches'};
+ $self->Limit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => 'query',
+ OPERATOR => '=',
+ VALUE => "$value;limit=$max;maxmatches=$max",
+ );
+ }
+ } else {
+ $self->Limit(
+ %rest,
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ );
+ }
+ if ( RT->Config->Get('DontSearchFileAttachments') ) {
+ $self->Limit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => 'Filename',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ }
+ $self->_CloseParen;
+}
+
+=head2 _WatcherLimit
+
+Handle watcher limits. (Requestor, CC, etc..)
+
+Meta Data:
+ 1: Field to query on
+
+
+
+=cut
+
+sub _WatcherLimit {
+ my $self = shift;
+ my $field = shift;
+ my $op = shift;
+ my $value = shift;
+ my %rest = (@_);
+
+ my $meta = $FIELD_METADATA{ $field };
+ my $type = $meta->[1] || '';
+ my $class = $meta->[2] || 'Ticket';
+
+ # Bail if the subfield is not allowed
+ if ( $rest{SUBKEY}
+ and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
+ {
+ die "Invalid watcher subfield: '$rest{SUBKEY}'";
+ }
+
+ $self->RoleLimit(
+ TYPE => $type,
+ CLASS => "RT::$class",
+ FIELD => $rest{SUBKEY},
+ OPERATOR => $op,
+ VALUE => $value,
+ SUBCLAUSE => "ticketsql",
+ %rest,
+ );
+}
+
+=head2 _WatcherMembershipLimit
+
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
+
+Meta Data:
+ 1: Role to query on
+
+=cut
+
+sub _WatcherMembershipLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ # we don't support anything but '='
+ die "Invalid $field Op: $op"
+ unless $op =~ /^=$/;
+
+ unless ( $value =~ /^\d+$/ ) {
+ my $group = RT::Group->new( $self->CurrentUser );
+ $group->LoadUserDefinedGroup( $value );
+ $value = $group->id || 0;
+ }
+
+ my $meta = $FIELD_METADATA{$field};
+ my $type = $meta->[1] || '';
+
+ my ($members_alias, $members_column);
+ if ( $type eq 'Owner' ) {
+ ($members_alias, $members_column) = ('main', 'Owner');
+ } else {
+ (undef, undef, $members_alias) = $self->_WatcherJoin( New => 1, Name => $type );
+ $members_column = 'id';
+ }
+
+ my $cgm_alias = $self->Join(
+ ALIAS1 => $members_alias,
+ FIELD1 => $members_column,
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'MemberId',
+ );
+ $self->Limit(
+ LEFTJOIN => $cgm_alias,
+ ALIAS => $cgm_alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+ $self->Limit(
+ ALIAS => $cgm_alias,
+ FIELD => 'GroupId',
+ VALUE => $value,
+ OPERATOR => $op,
+ %rest,
+ );
+}
+
+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
+
+Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
+
+=cut
+
+sub _CustomFieldDecipher {
+ my ($self, $string, $lookuptype) = @_;
+ $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
+
+ my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
+ $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
+
+ my ($cf, $applied_to);
+
+ if ( $object ) {
+ my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
+ $applied_to = $record_class->new( $self->CurrentUser );
+ $applied_to->Load( $object );
+
+ if ( $applied_to->id ) {
+ RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
+ }
+ else {
+ RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
+ $object = 0;
+ undef $applied_to;
+ }
+ }
+
+ if ( $field =~ /\D/ ) {
+ $object ||= '';
+ my $cfs = RT::CustomFields->new( $self->CurrentUser );
+ $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
+ $cfs->LimitToLookupType($lookuptype);
+
+ if ($applied_to) {
+ $cfs->SetContextObject($applied_to);
+ $cfs->LimitToObjectId($applied_to->id);
+ }
+
+ # 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 );
+ $cf->SetContextObject($applied_to)
+ if $cf->id and $applied_to;
+ }
+
+ return ($object, $field, $cf, $column);
+}
+
+=head2 _CustomFieldLimit
+
+Limit based on CustomFields
+
+Meta Data:
+ none
+
+=cut
+
+sub _CustomFieldLimit {
+ my ( $self, $_field, $op, $value, %rest ) = @_;
+
+ my $meta = $FIELD_METADATA{ $_field };
+ my $class = $meta->[1] || 'Ticket';
+ my $type = "RT::$class"->CustomFieldLookupType;
+
+ my $field = $rest{'SUBKEY'} || die "No field specified";
+
+ # For our sanity, we can only limit on one object at a time
+
+ my ($object, $cfid, $cf, $column);
+ ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
+
+
+ $self->_LimitCustomField(
+ %rest,
+ LOOKUPTYPE => $type,
+ CUSTOMFIELD => $cf || $field,
+ KEY => $cf ? $cf->id : "$type-$object.$field",
+ OPERATOR => $op,
+ VALUE => $value,
+ COLUMN => $column,
+ SUBCLAUSE => "ticketsql",
+ );
+}
+
+sub _CustomFieldJoinByName {
+ my $self = shift;
+ my ($ObjectAlias, $cf, $type) = @_;
+
+ my ($ocfvalias, $CFs, $ocfalias) = $self->SUPER::_CustomFieldJoinByName(@_);
+ $self->Limit(
+ LEFTJOIN => $ocfalias,
+ ENTRYAGGREGATOR => 'OR',
+ FIELD => 'ObjectId',
+ VALUE => 'main.Queue',
+ QUOTEVALUE => 0,
+ );
+ return ($ocfvalias, $CFs, $ocfalias);
+}
+
+sub _HasAttributeLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ my $alias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Attributes',
+ FIELD2 => 'ObjectId',
+ );
+ $self->Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'ObjectType',
+ VALUE => 'RT::Ticket',
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'Name',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->Limit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => 'id',
+ OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+}
+
+sub _LifecycleLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ die "Invalid Operator $op for $field" if $op =~ /^(IS|IS NOT)$/io;
+ my $queue = $self->{_sql_aliases}{queues} ||= $_[0]->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'Queue',
+ TABLE2 => 'Queues',
+ FIELD2 => 'id',
+ );
+
+ $self->Limit(
+ ALIAS => $queue,
+ FIELD => 'Lifecycle',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest,
+ );
+}
+
+# End Helper Functions
+
+# End of SQL Stuff -------------------------------------------------
+
+
+=head2 OrderByCols ARRAY
+
+A modified version of the OrderBy method which automatically joins where
+C<ALIAS> is set to the name of a watcher type.
+
+=cut
+
+sub OrderByCols {
+ my $self = shift;
+ my @args = @_;
+ my $clause;
+ my @res = ();
+ my $order = 0;
+
+ foreach my $row (@args) {
+ if ( $row->{ALIAS} ) {
+ push @res, $row;
+ next;
+ }
+ if ( $row->{FIELD} !~ /\./ ) {
+ my $meta = $FIELD_METADATA{ $row->{FIELD} };
+ unless ( $meta ) {
+ push @res, $row;
+ next;
+ }
+
+ if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
+ my $alias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => $row->{'FIELD'},
+ TABLE2 => 'Queues',
+ FIELD2 => 'id',
+ );
+ push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
+ } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
+ || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
+ ) {
+ my $alias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => $row->{'FIELD'},
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
+ } else {
+ push @res, $row;
+ }
+ next;
+ }
+
+ my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
+ my $meta = $FIELD_METADATA{$field};
+ if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
+ # cache alias as we want to use one alias per watcher type for sorting
+ my $cache_key = join "-", map { $_ || "" } @$meta[1,2];
+ my $users = $self->{_sql_u_watchers_alias_for_sort}{ $cache_key };
+ unless ( $users ) {
+ $self->{_sql_u_watchers_alias_for_sort}{ $cache_key }
+ = $users = ( $self->_WatcherJoin( Name => $meta->[1], Class => "RT::" . ($meta->[2] || 'Ticket') ) )[2];
+ }
+ push @res, { %$row, ALIAS => $users, FIELD => $subkey };
+ } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
+ my ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $subkey );
+ my $cfkey = $cf ? $cf->id : "$object.$field";
+ push @res, $self->_OrderByCF( $row, $cfkey, ($cf || $field) );
+ } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
+ # PAW logic is "reversed"
+ my $order = "ASC";
+ if (exists $row->{ORDER} ) {
+ my $o = $row->{ORDER};
+ delete $row->{ORDER};
+ $order = "DESC" if $o =~ /asc/i;
+ }
+
+ # Ticket.Owner 1 0 X
+ # Unowned Tickets 0 1 X
+ # Else 0 0 X
+
+ foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
+ if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+ my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
+ push @res, {
+ %$row,
+ FIELD => undef,
+ ALIAS => '',
+ FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
+ ORDER => $order
+ };
+ } else {
+ push @res, {
+ %$row,
+ FIELD => undef,
+ FUNCTION => "Owner=$uid",
+ ORDER => $order
+ };
+ }
+ }
+
+ push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
+
+ } elsif ( $field eq 'Customer' ) { #Freeside
+ # OrderBy(FIELD => expression) doesn't work, it has to be
+ # an actual field, so we have to do the join even if sorting
+ # by custnum
+ my $custalias = $self->JoinToCustomer;
+ my $cust_field = lc($subkey);
+ if ( !$cust_field or $cust_field eq 'number' ) {
+ $cust_field = 'custnum';
+ }
+ elsif ( $cust_field eq 'name' ) {
+ $cust_field = "COALESCE( $custalias.company,
+ $custalias.last || ', ' || $custalias.first
+ )";
+ }
+ else { # order by cust_main fields directly: 'Customer.agentnum'
+ $cust_field = $subkey;
+ }
+ push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
+
+ } elsif ( $field eq 'Service' ) {
+
+ my $svcalias = $self->JoinToService;
+ my $svc_field = lc($subkey);
+ if ( !$svc_field or $svc_field eq 'number' ) {
+ $svc_field = 'svcnum';
+ }
+ push @res, { %$row, ALIAS => $svcalias, FIELD => $svc_field };
+
+ } #Freeside
+
+ else {
+ push @res, $row;
+ }
+ }
+ return $self->SUPER::OrderByCols(@res);
+}
+
+sub _SQLLimit {
+ my $self = shift;
+ RT->Deprecated( Remove => "4.4", Instead => "Limit" );
+ $self->Limit(@_);
+}
+sub _SQLJoin {
+ my $self = shift;
+ RT->Deprecated( Remove => "4.4", Instead => "Join" );
+ $self->Join(@_);
+}
+
+sub _OpenParen {
+ $_[0]->SUPER::_OpenParen( $_[1] || 'ticketsql' );
+}
+sub _CloseParen {
+ $_[0]->SUPER::_CloseParen( $_[1] || 'ticketsql' );
+}
+