-#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Tickets.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+# Major Changes:
+
+# - Decimated ProcessRestrictions and broke it into multiple
+# functions joined by a LUT
+# - Semi-Generic SQL stuff moved to another file
+
+# Known Issues: FIXME!
+
+# - ClearRestrictions and Reinitialization is messy and unclear. The
+# only good way to do it is to create a new RT::Tickets object.
=head1 NAME
=head1 SYNOPSIS
use RT::Tickets;
- my $tickets = new RT::Tickets($CurrentUser);
+ my $tickets = RT::Tickets->new($CurrentUser);
=head1 DESCRIPTION
=head1 METHODS
-=begin testing
-
-ok (require RT::TestHarness);
-ok (require RT::Tickets);
-
-=end testing
=cut
package RT::Tickets;
-use RT::EasySearch;
+
+use strict;
+use warnings;
+
+
use RT::Ticket;
-@ISA= qw(RT::EasySearch);
-
-use vars qw(%TYPES @SORTFIELDS);
-
-# {{{ TYPES
-
-%TYPES = ( Status => 'ENUM',
- Queue => 'ENUM',
- Type => 'ENUM',
- Creator => 'ENUM',
- LastUpdatedBy => 'ENUM',
- Owner => 'ENUM',
- EffectiveId => 'INT',
- id => 'INT',
- InitialPriority => 'INT',
- FinalPriority => 'INT',
- Priority => 'INT',
- TimeLeft => 'INT',
- TimeWorked => 'INT',
- MemberOf => 'LINK',
- DependsOn => 'LINK',
- HasMember => 'LINK',
- HasDepender => 'LINK',
- RelatedTo => 'LINK',
- Told => 'DATE',
- StartsBy => 'DATE',
- Started => 'DATE',
- Due => 'DATE',
- Resolved => 'DATE',
- LastUpdated => 'DATE',
- Created => 'DATE',
- Subject => 'STRING',
- Type => 'STRING',
- Content => 'TRANSFIELD',
- ContentType => 'TRANSFIELD',
- TransactionDate => 'TRANSDATE',
- Watcher => 'WATCHERFIELD',
- LinkedTo => 'LINKFIELD',
- Keyword => 'KEYWORDFIELD'
-
- );
-
-
-# }}}
-
-# {{{ sub SortFields
-
-@SORTFIELDS = qw(id Status Owner Created Due Starts Started
- Queue Subject Told Started
- Resolved LastUpdated Priority TimeWorked TimeLeft);
+
+use base 'RT::SearchBuilder';
+
+sub Table { 'Tickets'}
+
+use RT::CustomFields;
+
+# Configuration Tables:
+
+# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
+# metadata.
+
+our %FIELD_METADATA = (
+ Status => [ 'ENUM', ], #loc_left_pair
+ Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
+ Type => [ 'ENUM', ], #loc_left_pair
+ Creator => [ 'ENUM' => 'User', ], #loc_left_pair
+ LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
+ Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
+ EffectiveId => [ 'INT', ], #loc_left_pair
+ id => [ 'ID', ], #loc_left_pair
+ InitialPriority => [ 'INT', ], #loc_left_pair
+ FinalPriority => [ 'INT', ], #loc_left_pair
+ Priority => [ 'INT', ], #loc_left_pair
+ TimeLeft => [ 'INT', ], #loc_left_pair
+ TimeWorked => [ 'INT', ], #loc_left_pair
+ TimeEstimated => [ 'INT', ], #loc_left_pair
+
+ Linked => [ 'LINK' ], #loc_left_pair
+ LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
+ LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
+ MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
+ DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
+ RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
+ HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
+ DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+ DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+ ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
+ Told => [ 'DATE' => 'Told', ], #loc_left_pair
+ Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
+ Started => [ 'DATE' => 'Started', ], #loc_left_pair
+ Due => [ 'DATE' => 'Due', ], #loc_left_pair
+ Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
+ LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
+ Created => [ 'DATE' => 'Created', ], #loc_left_pair
+ Subject => [ 'STRING', ], #loc_left_pair
+ Content => [ 'TRANSCONTENT', ], #loc_left_pair
+ ContentType => [ 'TRANSFIELD', ], #loc_left_pair
+ Filename => [ 'TRANSFIELD', ], #loc_left_pair
+ TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
+ Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
+ Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
+ Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
+ AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
+ Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
+ QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
+ QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
+ QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
+ CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ CustomField => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ CF => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ Updated => [ 'TRANSDATE', ], #loc_left_pair
+ RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
+ CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
+ AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
+ WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
+ HasAttribute => [ 'HASATTRIBUTE', 1 ],
+ HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
+ #freeside
+ Customer => [ 'FREESIDEFIELD' => 'Customer' ],
+ Service => [ 'FREESIDEFIELD' => 'Service' ],
+ WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
+);
+
+# Lower Case version of FIELDS, for case insensitivity
+our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
+
+our %SEARCHABLE_SUBFIELDS = (
+ User => [qw(
+ EmailAddress Name RealName Nickname Organization Address1 Address2
+ WorkPhone HomePhone MobilePhone PagerPhone id
+ )],
+);
+
+# Mapping of Field Type to Function
+our %dispatch = (
+ ENUM => \&_EnumLimit,
+ INT => \&_IntLimit,
+ ID => \&_IdLimit,
+ LINK => \&_LinkLimit,
+ DATE => \&_DateLimit,
+ STRING => \&_StringLimit,
+ TRANSFIELD => \&_TransLimit,
+ TRANSCONTENT => \&_TransContentLimit,
+ TRANSDATE => \&_TransDateLimit,
+ WATCHERFIELD => \&_WatcherLimit,
+ MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
+ CUSTOMFIELD => \&_CustomFieldLimit,
+ HASATTRIBUTE => \&_HasAttributeLimit,
+ FREESIDEFIELD => \&_FreesideFieldLimit,
+);
+our %can_bundle = ();# WATCHERFIELD => "yes", );
+
+# Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
+my %DefaultEA = (
+ INT => 'AND',
+ ENUM => {
+ '=' => 'OR',
+ '!=' => 'AND'
+ },
+ DATE => {
+ '=' => 'OR',
+ '>=' => 'AND',
+ '<=' => 'AND',
+ '>' => 'AND',
+ '<' => 'AND'
+ },
+ STRING => {
+ '=' => 'OR',
+ '!=' => 'AND',
+ 'LIKE' => 'AND',
+ 'NOT LIKE' => 'AND'
+ },
+ TRANSFIELD => 'AND',
+ TRANSDATE => 'AND',
+ LINK => 'OR',
+ LINKFIELD => 'AND',
+ TARGET => 'AND',
+ BASE => 'AND',
+ WATCHERFIELD => {
+ '=' => 'OR',
+ '!=' => 'AND',
+ 'LIKE' => 'OR',
+ 'NOT LIKE' => 'AND'
+ },
+
+ HASATTRIBUTE => {
+ '=' => 'AND',
+ '!=' => 'AND',
+ },
+
+ CUSTOMFIELD => 'OR',
+);
+
+# Helper functions for passing the above lexically scoped tables above
+# into Tickets_SQL.
+sub FIELDS { return \%FIELD_METADATA }
+sub dispatch { return \%dispatch }
+sub can_bundle { return \%can_bundle }
+
+# Bring in the clowns.
+require RT::Tickets_SQL;
+
+
+our @SORTFIELDS = qw(id Status
+ Queue Subject
+ Owner Created Due Starts Started
+ Told
+ Resolved LastUpdated Priority TimeWorked TimeLeft);
=head2 SortFields
=cut
-
sub SortFields {
- my $self = shift;
- return(@SORTFIELDS);
+ my $self = shift;
+ return (@SORTFIELDS);
}
-# }}}
+# BEGIN SQL STUFF *********************************
-# {{{ Limit the result set based on content
-# {{{ sub Limit
+sub CleanSlate {
+ my $self = shift;
+ $self->SUPER::CleanSlate( @_ );
+ delete $self->{$_} foreach qw(
+ _sql_cf_alias
+ _sql_group_members_aliases
+ _sql_object_cfv_alias
+ _sql_role_group_aliases
+ _sql_trattachalias
+ _sql_u_watchers_alias_for_sort
+ _sql_u_watchers_aliases
+ _sql_current_user_can_see_applied
+ );
+}
-=head2 Limit
+=head1 Limit Helper Routines
-Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
-Generally best called from LimitFoo methods
+These routines are the targets of a dispatch table depending on the
+type of field. They all share the same signature:
+
+ my ($self,$field,$op,$value,@rest) = @_;
+
+The values in @rest should be suitable for passing directly to
+DBIx::SearchBuilder::Limit.
+
+Essentially they are an expanded/broken out (and much simplified)
+version of what ProcessRestrictions used to do. They're also much
+more clearly delineated by the TYPE of field being processed.
+
+=head2 _IdLimit
+
+Handle ID field.
=cut
-sub Limit {
+
+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 = do {
+ my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
+ $tmp = $tmp->Content if $tmp;
+ $tmp ||= {};
+ grep $_, keys %$tmp;
+ };
+
+ return $sb->_SQLLimit(
+ 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',
+ );
+ $sb->_OpenParen;
+ my $first = 1;
+ my $ea = $op eq '='? 'OR': 'AND';
+ foreach my $id ( sort @bookmarks ) {
+ $sb->_SQLLimit(
+ ALIAS => $tickets_alias,
+ FIELD => 'id',
+ OPERATOR => $op,
+ VALUE => $id,
+ $first? (@rest): ( ENTRYAGGREGATOR => $ea )
+ );
+ $first = 0 if $first;
+ }
+ $sb->_CloseParen;
+}
+
+=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;
+ } elsif ($field eq "Status") {
+ $value = lc $value;
+ }
+ $sb->_SQLLimit(
+ 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 ) = @_;
+
+ die "Invalid Operator $op for $field"
+ unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+
+ $sb->_SQLLimit(
+ 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->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->_SQLLimit(
+ @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->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => '=',
+ VALUE => $value,
+ );
+ $sb->_SQLLimit(
+ @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 =~ /^(=|>|<|>=|<=)$/;
+
+ 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 ) = @_;
+
+ 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->_SQLLimit(
+ FIELD => $field,
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ @rest,
+ );
+
+ $sb->_SQLLimit(
+ FIELD => $field,
+ OPERATOR => "<",
+ VALUE => $dayend,
+ @rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ $sb->_CloseParen;
+
+ }
+ else {
+ $sb->_SQLLimit(
+ 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';
+ }
+
+ $sb->_SQLLimit(
+ 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->_SQLLimit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ @rest
+ );
+ $sb->_SQLLimit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => "<=",
+ VALUE => $dayend,
+ @rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ }
+
+ # not searching for a single day
+ else {
+
+ #Search for the right field
+ $sb->_SQLLimit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => $op,
+ VALUE => $date->ISO,
+ @rest
+ );
+ }
+
+ $sb->_CloseParen;
+}
+
+=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->_SQLJoin(
+ TYPE => 'LEFT', # not all txns have an attachment
+ ALIAS1 => $txn_alias,
+ FIELD1 => 'id',
+ TABLE2 => 'Attachments',
+ FIELD2 => 'TransactionId',
+ );
+ }
+
+ $self->_SQLLimit(
+ %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. We put them in TicketAliases so that they get nuked
+ #when we redo the join.
+
+ # 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->_SQLLimit( %rest, FIELD => 'id', VALUE => 0 );
+ return;
+ }
+
+ my $txn_alias = $self->JoinTransactions;
+ unless ( defined $self->{_sql_trattachalias} ) {
+ $self->{_sql_trattachalias} = $self->_SQLJoin(
+ 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->_SQLJoin(
+ 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->_SQLLimit(
+ %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->_SQLLimit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => 'Content',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ );
+ }
+ elsif ( $db_type eq 'Pg' ) {
+ my $dbh = $RT::Handle->dbh;
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => $index,
+ OPERATOR => '@@',
+ VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
+ 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->_SQLLimit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => 'query',
+ OPERATOR => '=',
+ VALUE => "$value;limit=$max;maxmatches=$max",
+ );
+ }
+ } else {
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $self->{_sql_trattachalias},
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ );
+ }
+ if ( RT->Config->Get('DontSearchFileAttachments') ) {
+ $self->_SQLLimit(
+ 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}'";
+ }
+
+ # if it's equality op and search by Email or Name then we can preload user
+ # we do it to help some DBs better estimate number of rows and get better plans
+ if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
+ my $o = RT::User->new( $self->CurrentUser );
+ my $method =
+ !$rest{'SUBKEY'}
+ ? $field eq 'Owner'? 'Load' : 'LoadByEmail'
+ : $rest{'SUBKEY'} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
+ $o->$method( $value );
+ $rest{'SUBKEY'} = 'id';
+ $value = $o->id || 0;
+ }
+
+ # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
+ # search by id and Name at the same time, this is workaround
+ # to preserve backward compatibility
+ if ( $field eq 'Owner' ) {
+ if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
+ $self->_SQLLimit(
+ FIELD => 'Owner',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest,
+ );
+ return;
+ }
+ }
+ $rest{SUBKEY} ||= 'EmailAddress';
+
+ my ($groups, $group_members, $users);
+ if ( $rest{'BUNDLE'} ) {
+ ($groups, $group_members, $users) = @{ $rest{'BUNDLE'} };
+ } else {
+ $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
+ }
+
+ $self->_OpenParen;
+ if ( $op =~ /^IS(?: NOT)?$/i ) {
+ # is [not] empty case
+
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+ # to avoid joining the table Users into the query, we just join GM
+ # and make sure we don't match records where group is member of itself
+ $self->SUPER::Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ $self->_SQLLimit(
+ ALIAS => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest,
+ );
+ }
+ elsif ( $op =~ /^!=$|^NOT\s+/i ) {
+ # negative condition case
+
+ # reverse op
+ $op =~ s/!|NOT\s+//i;
+
+ # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
+ # "X = 'Y'" matches more then one user so we try to fetch two records and
+ # do the right thing when there is only one exist and semi-working solution
+ # otherwise.
+ my $users_obj = RT::Users->new( $self->CurrentUser );
+ $users_obj->Limit(
+ FIELD => $rest{SUBKEY},
+ OPERATOR => $op,
+ VALUE => $value,
+ );
+ $users_obj->OrderBy;
+ $users_obj->RowsPerPage(2);
+ my @users = @{ $users_obj->ItemsArrayRef };
+
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+ if ( @users <= 1 ) {
+ my $uid = 0;
+ $uid = $users[0]->id if @users;
+ $self->SUPER::Limit(
+ LEFTJOIN => $group_members,
+ ALIAS => $group_members,
+ FIELD => 'MemberId',
+ VALUE => $uid,
+ );
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $group_members,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ } else {
+ $self->SUPER::Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ $users ||= $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $users,
+ ALIAS => $users,
+ FIELD => $rest{SUBKEY},
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ );
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $users,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ }
+ } else {
+ # positive condition case
+
+ $group_members ||= $self->_GroupMembersJoin(
+ GroupsAlias => $groups, New => 1, Left => 0
+ );
+ $users ||= $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $users,
+ FIELD => $rest{'SUBKEY'},
+ VALUE => $value,
+ OPERATOR => $op,
+ CASESENSITIVE => 0,
+ );
+ }
+ $self->_CloseParen;
+ return ($groups, $group_members, $users);
+}
+
+sub _RoleGroupsJoin {
my $self = shift;
- my %args = ( FIELD => undef,
- OPERATOR => '=',
- VALUE => undef,
- DESCRIPTION => undef,
- @_
- );
- $args{'DESCRIPTION'} = "Autodescribed: ".$args{'FIELD'} . $args{'OPERATOR'} . $args{'VALUE'},
- if (!defined $args{'DESCRIPTION'}) ;
+ my %args = (New => 0, Class => 'Ticket', Type => '', @_);
+ return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+ if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+ && !$args{'New'};
+
+ # we always have watcher groups for ticket, so we use INNER join
+ my $groups = $self->Join(
+ ALIAS1 => 'main',
+ FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
+ TABLE2 => 'Groups',
+ FIELD2 => 'Instance',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $groups,
+ ALIAS => $groups,
+ FIELD => 'Domain',
+ VALUE => 'RT::'. $args{'Class'} .'-Role',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $groups,
+ ALIAS => $groups,
+ FIELD => 'Type',
+ VALUE => $args{'Type'},
+ ) if $args{'Type'};
+
+ $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
+ unless $args{'New'};
+
+ return $groups;
+}
- my $index = $self->_NextIndex;
-
- #make the TicketRestrictions hash the equivalent of whatever we just passed in;
-
- %{$self->{'TicketRestrictions'}{$index}} = %args;
+sub _GroupMembersJoin {
+ my $self = shift;
+ my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
+
+ return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+ if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+ && !$args{'New'};
+
+ my $alias = $self->Join(
+ $args{'Left'} ? (TYPE => 'LEFT') : (),
+ ALIAS1 => $args{'GroupsAlias'},
+ FIELD1 => 'id',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ $self->SUPER::Limit(
+ $args{'Left'} ? (LEFTJOIN => $alias) : (),
+ ALIAS => $alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+ $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
+ unless $args{'New'};
+
+ return $alias;
+}
- $self->{'RecalcTicketLimits'} = 1;
+=head2 _WatcherJoin
- # If we're looking at the effective id, we don't want to append the other clause
- # which limits us to tickets where id = effective id
- if ($args{'FIELD'} eq 'EffectiveId') {
- $self->{'looking_at_effective_id'} = 1;
+Helper function which provides joins to a watchers table both for limits
+and for ordering.
+
+=cut
+
+sub _WatcherJoin {
+ my $self = shift;
+ my $type = shift || '';
+
+
+ my $groups = $self->_RoleGroupsJoin( Type => $type );
+ my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+ # XXX: work around, we must hide groups that
+ # are members of the role group we search in,
+ # otherwise them result in wrong NULLs in Users
+ # table and break ordering. Now, we know that
+ # RT doesn't allow to add groups as members of the
+ # ticket roles, so we just hide entries in CGM table
+ # with MemberId == GroupId from results
+ $self->SUPER::Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ my $users = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ return ($groups, $group_members, $users);
+}
+
+=head2 _WatcherMembershipLimit
+
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
+
+Meta Data:
+ 1: Field to query on
+
+SELECT DISTINCT main.*
+FROM
+ Tickets main,
+ Groups Groups_1,
+ CachedGroupMembers CachedGroupMembers_2,
+ Users Users_3
+WHERE (
+ (main.EffectiveId = main.id)
+) AND (
+ (main.Status != 'deleted')
+) AND (
+ (main.Type = 'ticket')
+) AND (
+ (
+ (Users_3.EmailAddress = '22')
+ AND
+ (Groups_1.Domain = 'RT::Ticket-Role')
+ AND
+ (Groups_1.Type = 'RequestorGroup')
+ )
+) AND
+ Groups_1.Instance = main.id
+AND
+ Groups_1.id = CachedGroupMembers_2.GroupId
+AND
+ CachedGroupMembers_2.MemberId = Users_3.id
+ORDER BY main.id ASC
+LIMIT 25
+
+=cut
+
+sub _WatcherMembershipLimit {
+ my ( $self, $field, $op, $value, @rest ) = @_;
+ my %rest = @rest;
+
+ $self->_OpenParen;
+
+ my $groups = $self->NewAlias('Groups');
+ my $groupmembers = $self->NewAlias('CachedGroupMembers');
+ my $users = $self->NewAlias('Users');
+ my $memberships = $self->NewAlias('CachedGroupMembers');
+
+ if ( ref $field ) { # gross hack
+ my @bundle = @$field;
+ $self->_OpenParen;
+ for my $chunk (@bundle) {
+ ( $field, $op, $value, @rest ) = @$chunk;
+ $self->_SQLLimit(
+ ALIAS => $memberships,
+ FIELD => 'GroupId',
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
+ }
+ $self->_CloseParen;
+ }
+ else {
+ $self->_SQLLimit(
+ ALIAS => $memberships,
+ FIELD => 'GroupId',
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
}
- return ($index);
+ # Tie to groups for tickets we care about
+ $self->_SQLLimit(
+ ALIAS => $groups,
+ FIELD => 'Domain',
+ VALUE => 'RT::Ticket-Role',
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ $self->Join(
+ ALIAS1 => $groups,
+ FIELD1 => 'Instance',
+ ALIAS2 => 'main',
+ FIELD2 => 'id'
+ );
+
+ # }}}
+
+ # If we care about which sort of watcher
+ my $meta = $FIELD_METADATA{$field};
+ my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+
+ if ($type) {
+ $self->_SQLLimit(
+ ALIAS => $groups,
+ FIELD => 'Type',
+ VALUE => $type,
+ ENTRYAGGREGATOR => 'AND'
+ );
+ }
+
+ $self->Join(
+ ALIAS1 => $groups,
+ FIELD1 => 'id',
+ ALIAS2 => $groupmembers,
+ FIELD2 => 'GroupId'
+ );
+
+ $self->Join(
+ ALIAS1 => $groupmembers,
+ FIELD1 => 'MemberId',
+ ALIAS2 => $users,
+ FIELD2 => 'id'
+ );
+
+ $self->Limit(
+ ALIAS => $groupmembers,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+ $self->Join(
+ ALIAS1 => $memberships,
+ FIELD1 => 'MemberId',
+ ALIAS2 => $users,
+ FIELD2 => 'id'
+ );
+
+ $self->Limit(
+ ALIAS => $memberships,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+
+ $self->_CloseParen;
+
}
-# }}}
+=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;
-=head2 FreezeLimits
+ my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
+ $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
-Returns a frozen string suitable for handing back to ThawLimits.
+ 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, ($applied_to ? (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 _CustomFieldJoin
+
+Factor out the Join of custom fields so we can use it for sorting too
=cut
-# {{{ sub FreezeLimits
-sub FreezeLimits {
- my $self = shift;
- require FreezeThaw;
- return (FreezeThaw::freeze($self->{'TicketRestrictions'},
- $self->{'restriction_index'}
- ));
+our %JOIN_ALIAS_FOR_LOOKUP_TYPE = (
+ RT::Ticket->CustomFieldLookupType => sub { "main" },
+);
+
+sub _CustomFieldJoin {
+ my ($self, $cfkey, $cfid, $field, $type) = @_;
+ $type ||= RT::Ticket->CustomFieldLookupType;
+
+ # 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 $ObjectAlias = $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}
+ ? $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}->($self)
+ : die "We don't know how to join on $type";
+
+ my ($ObjectCFs, $CFs);
+ if ( $cfid ) {
+ $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $ObjectAlias,
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'ObjectId',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $ObjectCFs,
+ 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 => $type,
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $CFs,
+ ENTRYAGGREGATOR => 'AND',
+ FIELD => 'Name',
+ VALUE => $field,
+ );
+
+ $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $CFs,
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'CustomField',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $ObjectCFs,
+ FIELD => 'ObjectId',
+ VALUE => "$ObjectAlias.id",
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ }
+
+ $self->SUPER::Limit(
+ LEFTJOIN => $ObjectCFs,
+ FIELD => 'ObjectType',
+ VALUE => RT::CustomField->ObjectTypeFromLookupType($type),
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $ObjectCFs,
+ FIELD => 'Disabled',
+ OPERATOR => '=',
+ VALUE => '0',
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ return ($ObjectCFs, $CFs);
}
-# }}}
+=head2 _CustomFieldLimit
-=head2 ThawLimits
+Limit based on CustomFields
-Take a frozen Limits string generated by FreezeLimits and make this tickets
-object have that set of limits.
+Meta Data:
+ none
=cut
-# {{{ sub ThawLimits
-sub ThawLimits {
- my $self = shift;
- my $in = shift;
-
- #if we don't have $in, get outta here.
- return undef unless ($in);
+use Regexp::Common qw(RE_net_IPv4);
+use Regexp::Common::net::CIDR;
+
+
+sub _CustomFieldLimit {
+ my ( $self, $_field, $op, $value, %rest ) = @_;
+
+ my $meta = $FIELD_METADATA{ $_field };
+ my $class = $meta->[1] || 'Ticket';
+ my $type = "RT::$class"->CustomFieldLookupType;
- $self->{'RecalcTicketLimits'} = 1;
+ my $field = $rest{'SUBKEY'} || die "No field specified";
- require FreezeThaw;
-
- #We don't need to die if the thaw fails.
-
- eval {
- ($self->{'TicketRestrictions'},
- $self->{'restriction_index'}
- ) = FreezeThaw::thaw($in);
- }
+ # For our sanity, we can only limit on one queue at a time
+ my ($object, $cfid, $cf, $column);
+ ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
+ $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 {
+ return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
+
+ my %args = @_;
+ return %args unless $args{'FIELD'} eq 'LargeContent';
+
+ my $op = $args{'OPERATOR'};
+ if ( $op eq '=' ) {
+ $args{'OPERATOR'} = 'MATCHES';
+ }
+ elsif ( $op eq '!=' ) {
+ $args{'OPERATOR'} = 'NOT MATCHES';
+ }
+ elsif ( $op =~ /^[<>]=?$/ ) {
+ $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
+ }
+ return %args;
+ };
+
+ if ( $cf && $cf->Type eq 'IPAddress' ) {
+ my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
+ if ($parsed) {
+ $value = $parsed;
+ }
+ else {
+ $RT::Logger->warn("$value is not a valid IPAddress");
+ }
+ }
+
+ if ( $cf && $cf->Type eq 'IPAddressRange' ) {
+ my ( $start_ip, $end_ip ) =
+ RT::ObjectCustomFieldValue->ParseIPRange($value);
+ if ( $start_ip && $end_ip ) {
+ if ( $op =~ /^([<>])=?$/ ) {
+ my $is_less = $1 eq '<' ? 1 : 0;
+ if ( $is_less ) {
+ $value = $start_ip;
+ }
+ else {
+ $value = $end_ip;
+ }
+ }
+ else {
+ $value = join '-', $start_ip, $end_ip;
+ }
+ }
+ else {
+ $RT::Logger->warn("$value is not a valid IPAddressRange");
+ }
+ }
+
+ if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+ if ( $date->Unix ) {
+
+ if (
+ $cf->Type eq 'Date'
+ || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
+ || ( $value !~ /midnight|\d+:\d+:\d+/i
+ && $date->Time( Timezone => 'user' ) eq '00:00:00' )
+ )
+ {
+ $value = $date->Date( Timezone => 'user' );
+ }
+ else {
+ $value = $date->DateTime;
+ }
+ }
+ else {
+ $RT::Logger->warn("$value is not a valid date string");
+ }
+ }
+
+ my $single_value = !$cf || !$cfid || $cf->SingleValue;
+
+ my $cfkey = $cfid ? $cfid : "$type-$object.$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 ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ 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 ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
+
+ my ($start_ip, $end_ip) = split /-/, $value;
+
+ $self->_OpenParen;
+ if ( $op !~ /NOT|!=|<>/i ) { # positive equation
+ $self->_CustomFieldLimit(
+ $_field, '<=', $end_ip, %rest,
+ SUBKEY => $rest{'SUBKEY'}. '.Content',
+ );
+ $self->_CustomFieldLimit(
+ $_field, '>=', $start_ip, %rest,
+ SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ # as well limit borders so DB optimizers can use better
+ # estimations and scan less rows
+# have to disable this tweak because of ipv6
+# $self->_CustomFieldLimit(
+# $_field, '>=', '000.000.000.000', %rest,
+# SUBKEY => $rest{'SUBKEY'}. '.Content',
+# ENTRYAGGREGATOR => 'AND',
+# );
+# $self->_CustomFieldLimit(
+# $_field, '<=', '255.255.255.255', %rest,
+# SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
+# ENTRYAGGREGATOR => 'AND',
+# );
+ }
+ else { # negative equation
+ $self->_CustomFieldLimit($_field, '>', $end_ip, %rest);
+ $self->_CustomFieldLimit(
+ $_field, '<', $start_ip, %rest,
+ SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
+ ENTRYAGGREGATOR => 'OR',
+ );
+ # TODO: as well limit borders so DB optimizers can use better
+ # estimations and scan less rows, but it's harder to do
+ # as we have OR aggregator
+ }
+ $self->_CloseParen;
+ }
+ elsif ( !$negative_op || $single_value ) {
+ $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+
+ $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( $fix_op->(
+ ALIAS => $ObjectCFs,
+ FIELD => $column,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ %rest
+ ) );
+ $self->_CloseParen;
+ $self->_CloseParen;
+ $self->_CloseParen;
+ }
+ else {
+ # need special treatment for Date
+ if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
+ # no time specified, that means we want everything on a
+ # particular day. in the database, we need to check for >
+ # and < the edges of that day.
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+ my $daystart = $date->ISO;
+ $date->AddDay;
+ my $dayend = $date->ISO;
+
+ $self->_OpenParen;
+
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ %rest,
+ );
+
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => "<",
+ VALUE => $dayend,
+ %rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ $self->_CloseParen;
+ }
+ elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+ if ( length( Encode::encode( "UTF-8", $value) ) < 256 ) {
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ %rest
+ );
+ }
+ else {
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit( $fix_op->(
+ ALIAS => $ObjectCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
+ ) );
+ }
+ }
+ else {
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ %rest
+ );
+
+ $self->_OpenParen;
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit( $fix_op->(
+ ALIAS => $ObjectCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
+ ) );
+ $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,
+ ENTRYAGGREGATOR => 'AND',
+ ) if $CFs;
+ $self->_CloseParen;
+
+ if ($negative_op) {
+ $self->_SQLLimit(
+ ALIAS => $ObjectCFs,
+ FIELD => $column || 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'OR',
+ );
+ }
+
+ $self->_CloseParen;
+ }
+ }
+ else {
+ $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+
+ # reverse operation
+ $op =~ s/!|NOT\s+//i;
+
+ # if column is defined then deal only with it
+ # otherwise search in Content and in LargeContent
+ if ( $column ) {
+ $self->SUPER::Limit( $fix_op->(
+ LEFTJOIN => $ObjectCFs,
+ ALIAS => $ObjectCFs,
+ FIELD => $column,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ ) );
+ }
+ else {
+ $self->SUPER::Limit(
+ LEFTJOIN => $ObjectCFs,
+ ALIAS => $ObjectCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ );
+ }
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $ObjectCFs,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+}
+
+sub _HasAttributeLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ my $alias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Attributes',
+ FIELD2 => 'ObjectId',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'ObjectType',
+ VALUE => 'RT::Ticket',
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $alias,
+ FIELD => 'Name',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND'
+ );
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $alias,
+ FIELD => 'id',
+ OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+}
+
+# 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 = $self->FIELDS->{ $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" };
+ } 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" };
+ } else {
+ push @res, $row;
+ }
+ next;
+ }
+
+ my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
+ my $meta = $self->FIELDS->{$field};
+ if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
+ # cache alias as we want to use one alias per watcher type for sorting
+ my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
+ unless ( $users ) {
+ $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
+ = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
+ }
+ push @res, { %$row, ALIAS => $users, FIELD => $subkey };
+ } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
+ my ($object, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
+ my $cfkey = $cf_obj ? $cf_obj->id : "$object.$field";
+ $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
+ # this is described in _CustomFieldLimit
+ $self->_SQLLimit(
+ ALIAS => $CFs,
+ FIELD => 'Name',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 1,
+ ENTRYAGGREGATOR => 'AND',
+ ) if $CFs;
+ my $CFvs = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $ObjectCFs,
+ FIELD1 => 'CustomField',
+ TABLE2 => 'CustomFieldValues',
+ FIELD2 => 'CustomField',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $CFvs,
+ FIELD => 'Name',
+ QUOTEVALUE => 0,
+ VALUE => $ObjectCFs . ".Content",
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
+ push @res, { %$row, ALIAS => $ObjectCFs, FIELD => 'Content' };
+ } 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);
+}
+
+#Freeside
+
+sub JoinToCustLinks {
+ # Set up join to links (id = localbase),
+ # limit link type to 'MemberOf',
+ # and target value to any Freeside custnum URI.
+ # Return the linkalias for further join/limit action,
+ # and an sql expression to retrieve the custnum.
+ my $self = shift;
+ # only join once for each RT::Tickets object
+ my $linkalias = $self->{cust_main_linkalias};
+ if (!$linkalias) {
+ $linkalias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => 'LocalBase',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Base',
+ OPERATOR => 'LIKE',
+ VALUE => 'fsck.com-rt://%/ticket/%',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => 'MemberOf',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Target',
+ OPERATOR => 'STARTSWITH',
+ VALUE => 'freeside://freeside/cust_main/',
+ );
+ $self->{cust_main_linkalias} = $linkalias;
+ }
+ my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
+ if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
+ $custnum_sql .= 'SIGNED INTEGER)';
+ }
+ else {
+ $custnum_sql .= 'INTEGER)';
+ }
+ return ($linkalias, $custnum_sql);
+}
+
+sub JoinToCustomer {
+ my $self = shift;
+ my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
+ # don't reuse this join, though--negative queries need
+ # independent joins
+ my $custalias = $self->Join(
+ TYPE => 'LEFT',
+ EXPRESSION => $custnum_sql,
+ TABLE2 => 'cust_main',
+ FIELD2 => 'custnum',
+ );
+ return $custalias;
+}
+
+sub JoinToSvcLinks {
+ my $self = shift;
+ my $linkalias = $self->{cust_svc_linkalias};
+ if (!$linkalias) {
+ $linkalias = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => 'LocalBase',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Base',
+ OPERATOR => 'LIKE',
+ VALUE => 'fsck.com-rt://%/ticket/%',
+ );
+
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => 'MemberOf',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Target',
+ OPERATOR => 'STARTSWITH',
+ VALUE => 'freeside://freeside/cust_svc/',
+ );
+ $self->{cust_svc_linkalias} = $linkalias;
+ }
+ my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
+ if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
+ $svcnum_sql .= 'SIGNED INTEGER)';
+ }
+ else {
+ $svcnum_sql .= 'INTEGER)';
+ }
+ return ($linkalias, $svcnum_sql);
+}
+
+sub JoinToService {
+ my $self = shift;
+ my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
+ $self->Join(
+ TYPE => 'LEFT',
+ EXPRESSION => $svcnum_sql,
+ TABLE2 => 'cust_svc',
+ FIELD2 => 'svcnum',
+ );
+}
+
+# This creates an alternate left join path to cust_main via cust_svc.
+# _FreesideFieldLimit needs to add this as a separate, independent join
+# and include all tickets that have a matching cust_main record via
+# either path.
+sub JoinToCustomerViaService {
+ my $self = shift;
+ my $svcalias = $self->JoinToService;
+ my $cust_pkg = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $svcalias,
+ FIELD1 => 'pkgnum',
+ TABLE2 => 'cust_pkg',
+ FIELD2 => 'pkgnum',
+ );
+ my $cust_main = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $cust_pkg,
+ FIELD1 => 'custnum',
+ TABLE2 => 'cust_main',
+ FIELD2 => 'custnum',
+ );
+ $cust_main;
+}
+
+sub _FreesideFieldLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+ my $is_negative = 0;
+ if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+ # if the op is negative, do the join as though
+ # the op were positive, then accept only records
+ # where the right-side join key is null.
+ $is_negative = 1;
+ $op = '=' if $op eq '!=';
+ $op =~ s/\bNOT\b//;
+ }
+
+ my (@alias, $table2, $subfield, $pkey);
+ if ( $field eq 'Customer' ) {
+ push @alias, $self->JoinToCustomer;
+ push @alias, $self->JoinToCustomerViaService;
+ $pkey = 'custnum';
+ }
+ elsif ( $field eq 'Service' ) {
+ push @alias, $self->JoinToService;
+ $pkey = 'svcnum';
+ }
+ else {
+ die "malformed Freeside query: $field";
+ }
+
+ $subfield = $rest{SUBKEY} || $pkey;
+ # compound subkey: separate into table name and field in that table
+ # (must be linked by custnum)
+ $subfield = lc($subfield);
+ ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
+ $subfield = $pkey if $subfield eq 'number';
+
+ # if it's compound, create a join from cust_main or cust_svc to that
+ # table, using custnum or svcnum, and Limit on that table instead.
+ my @_SQLLimit = ();
+ foreach my $a (@alias) {
+ if ( $table2 ) {
+ $a = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $a,
+ FIELD1 => $pkey,
+ TABLE2 => $table2,
+ FIELD2 => $pkey,
+ );
+ }
+
+ # do the actual Limit
+ $self->SUPER::Limit(
+ LEFTJOIN => $a,
+ FIELD => $subfield,
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ # no SUBCLAUSE needed, limits on different aliases across left joins
+ # are inherently independent
+ );
+
+ # then, since it's a left join, exclude tickets for which there is now
+ # no matching record in the table we just limited on. (Or where there
+ # is a matching record, if $is_negative.)
+ # For a cust_main query (where there are two different aliases), this
+ # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR
+ # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
+ # query).
+ #$self->_SQLLimit(
+ push @_SQLLimit, {
+ %rest,
+ ALIAS => $a,
+ FIELD => $pkey,
+ OPERATOR => $is_negative ? 'IS' : 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
+ SUBCLAUSE => 'fs_limit',
+ };
+ }
+
+ $self->_OpenParen;
+ foreach my $_SQLLimit (@_SQLLimit) {
+ $self->_SQLLimit( %$_SQLLimit);
+ }
+ $self->_CloseParen;
+
+}
+
+#Freeside
+
+=head2 Limit
+
+Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
+Generally best called from LimitFoo methods
+
+=cut
+
+sub Limit {
+ my $self = shift;
+ my %args = (
+ FIELD => undef,
+ OPERATOR => '=',
+ VALUE => undef,
+ DESCRIPTION => undef,
+ @_
+ );
+ $args{'DESCRIPTION'} = $self->loc(
+ "[_1] [_2] [_3]", $args{'FIELD'},
+ $args{'OPERATOR'}, $args{'VALUE'}
+ )
+ if ( !defined $args{'DESCRIPTION'} );
+
+ my $index = $self->_NextIndex;
+
+# make the TicketRestrictions hash the equivalent of whatever we just passed in;
+
+ %{ $self->{'TicketRestrictions'}{$index} } = %args;
+
+ $self->{'RecalcTicketLimits'} = 1;
+
+# If we're looking at the effective id, we don't want to append the other clause
+# which limits us to tickets where id = effective id
+ if ( $args{'FIELD'} eq 'EffectiveId'
+ && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
+ {
+ $self->{'looking_at_effective_id'} = 1;
+ }
+
+ if ( $args{'FIELD'} eq 'Type'
+ && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
+ {
+ $self->{'looking_at_type'} = 1;
+ }
+
+ return ($index);
}
-# }}}
-# {{{ Limit by enum or foreign key
-# {{{ sub LimitQueue
=head2 LimitQueue
LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of = or !=. (It defaults to =).
-VALUE is a queue id.
+VALUE is a queue id or Name.
+
=cut
sub LimitQueue {
my $self = shift;
- my %args = (VALUE => undef,
- OPERATOR => '=',
- @_);
+ my %args = (
+ VALUE => undef,
+ OPERATOR => '=',
+ @_
+ );
+
+ #TODO VALUE should also take queue objects
+ if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
+ my $queue = RT::Queue->new( $self->CurrentUser );
+ $queue->Load( $args{'VALUE'} );
+ $args{'VALUE'} = $queue->Id;
+ }
+
+ # What if they pass in an Id? Check for isNum() and convert to
+ # string.
- #TODO VALUE should also take queue names and queue objects
- my $queue = new RT::Queue($self->CurrentUser);
- $queue->Load($args{'VALUE'});
-
#TODO check for a valid queue here
- $self->Limit (FIELD => 'Queue',
- VALUE => $queue->id(),
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Queue ' . $args{'OPERATOR'}. " ". $queue->Name
- );
-
+ $self->Limit(
+ FIELD => 'Queue',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join(
+ ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
+ ),
+ );
+
}
-# }}}
-# {{{ sub LimitStatus
+
=head2 LimitStatus
OPERATOR is one of = or !=.
VALUE is a status.
+RT adds Status != 'deleted' until object has
+allow_deleted_search internal property set.
+$tickets->{'allow_deleted_search'} = 1;
+$tickets->LimitStatus( VALUE => 'deleted' );
+
=cut
sub LimitStatus {
my $self = shift;
- my %args = ( OPERATOR => '=',
- @_);
- $self->Limit (FIELD => 'Status',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Status ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ my %args = (
+ OPERATOR => '=',
+ @_
+ );
+ $self->Limit(
+ FIELD => 'Status',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Status'), $args{'OPERATOR'},
+ $self->loc( $args{'VALUE'} ) ),
+ );
+}
+
+
+
+=head2 IgnoreType
+
+If called, this search will not automatically limit the set of results found
+to tickets of type "Ticket". Tickets of other types, such as "project" and
+"approval" will be found.
+
+=cut
+
+sub IgnoreType {
+ my $self = shift;
+
+ # Instead of faking a Limit that later gets ignored, fake up the
+ # fact that we're already looking at type, so that the check in
+ # Tickets_SQL/FromSQL goes down the right branch
+
+ # $self->LimitType(VALUE => '__any');
+ $self->{looking_at_type} = 1;
}
-# }}}
-# {{{ sub LimitType
=head2 LimitType
OPERATOR is one of = or !=, it defaults to "=".
VALUE is a string to search for in the type of the ticket.
+
+
=cut
sub LimitType {
my $self = shift;
- my %args = (OPERATOR => '=',
- VALUE => undef,
- @_);
- $self->Limit (FIELD => 'Type',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Type ' . $args{'OPERATOR'}. " ". $args{'Limit'},
- );
+ my %args = (
+ OPERATOR => '=',
+ VALUE => undef,
+ @_
+ );
+ $self->Limit(
+ FIELD => 'Type',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# }}}
-# {{{ Limit by string field
-# {{{ sub LimitSubject
=head2 LimitSubject
sub LimitSubject {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'Subject',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Subject ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'Subject',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# }}}
-# {{{ Limit based on ticket numerical attributes
# Things that can be > < = !=
-# {{{ sub LimitId
=head2 LimitId
sub LimitId {
my $self = shift;
- my %args = (OPERATOR => '=',
- @_);
-
- $self->Limit (FIELD => 'id',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Id ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ my %args = (
+ OPERATOR => '=',
+ @_
+ );
+
+ $self->Limit(
+ FIELD => 'id',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION =>
+ join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitPriority
=head2 LimitPriority
Takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s priority against
+VALUE is a value to match the ticket's priority against
=cut
sub LimitPriority {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'Priority',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'Priority',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Priority'),
+ $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitInitialPriority
=head2 LimitInitialPriority
Takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s initial priority against
+VALUE is a value to match the ticket's initial priority against
=cut
sub LimitInitialPriority {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'InitialPriority',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Initial Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'InitialPriority',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Initial Priority'), $args{'OPERATOR'},
+ $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitFinalPriority
=head2 LimitFinalPriority
Takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s final priority against
+VALUE is a value to match the ticket's final priority against
=cut
sub LimitFinalPriority {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'FinalPriority',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Final Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'FinalPriority',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Final Priority'), $args{'OPERATOR'},
+ $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitTimeWorked
=head2 LimitTimeWorked
sub LimitTimeWorked {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'TimeWorked',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Time worked ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'TimeWorked',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Time Worked'),
+ $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitTimeLeft
=head2 LimitTimeLeft
sub LimitTimeLeft {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'TimeLeft',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Time left ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'TimeLeft',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Time Left'),
+ $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# }}}
-# {{{ Limiting based on attachment attributes
-# {{{ sub LimitContent
=head2 LimitContent
VALUE is a string to search for in the body of the ticket
=cut
-sub LimitContent {
- my $self = shift;
- my %args = (@_);
- $self->Limit (FIELD => 'Content',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Ticket content ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
-}
-
-# }}}
-# {{{ sub LimitContentType
-=head2 LimitContentType
-
-Takes a paramhash with the fields OPERATOR and VALUE.
-OPERATOR is one of =, LIKE, NOT LIKE or !=.
-VALUE is a content type to search ticket attachments for
-
-=cut
-
-sub LimitContentType {
+sub LimitContent {
my $self = shift;
my %args = (@_);
- $self->Limit (FIELD => 'ContentType',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Ticket content type ' . $args{'OPERATOR'}. " ". $args{'VALUE'},
- );
+ $self->Limit(
+ FIELD => 'Content',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Ticket content'), $args{'OPERATOR'},
+ $args{'VALUE'}, ),
+ );
}
-# }}}
-
-# }}}
-# {{{ Limiting based on people
-# {{{ sub LimitOwner
-=head2 LimitOwner
+=head2 LimitFilename
Takes a paramhash with the fields OPERATOR and VALUE.
-OPERATOR is one of = or !=.
-VALUE is a user id.
-
-=cut
-
-sub LimitOwner {
- my $self = shift;
- my %args = ( OPERATOR => '=',
- @_);
-
- my $owner = new RT::User($self->CurrentUser);
- $owner->Load($args{'VALUE'});
- $self->Limit (FIELD => 'Owner',
- VALUE => $owner->Id,
- OPERATOR => $args{'OPERATOR'},
- DESCRIPTION => 'Owner ' . $args{'OPERATOR'}. " ". $owner->Name()
- );
-
-}
-
-# }}}
-
-# {{{ Limiting watchers
-
-# {{{ sub LimitWatcher
-
-
-=head2 LimitWatcher
-
- Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
- OPERATOR is one of =, LIKE, NOT LIKE or !=.
- VALUE is a value to match the ticket\'s watcher email addresses against
- TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
-
-=cut
-
-sub LimitWatcher {
- my $self = shift;
- my %args = ( OPERATOR => '=',
- VALUE => undef,
- TYPE => undef,
- @_);
-
-
- #build us up a description
- my ($watcher_type, $desc);
- if ($args{'TYPE'}) {
- $watcher_type = $args{'TYPE'};
- }
- else {
- $watcher_type = "Watcher";
- }
- $desc = "$watcher_type ".$args{'OPERATOR'}." ".$args{'VALUE'};
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a string to search for in the body of the ticket
+=cut
- $self->Limit (FIELD => 'Watcher',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
- TYPE => $args{'TYPE'},
- DESCRIPTION => "$desc"
- );
+sub LimitFilename {
+ my $self = shift;
+ my %args = (@_);
+ $self->Limit(
+ FIELD => 'Filename',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Attachment filename'), $args{'OPERATOR'},
+ $args{'VALUE'}, ),
+ );
}
-# }}}
-
-# {{{ sub LimitRequestor
-=head2 LimitRequestor
+=head2 LimitContentType
-It\'s like LimitWatcher, but it presets TYPE to Requestor
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a content type to search ticket attachments for
=cut
-
-sub LimitRequestor {
+sub LimitContentType {
my $self = shift;
- $self->LimitWatcher(TYPE=> 'Requestor', @_);
+ my %args = (@_);
+ $self->Limit(
+ FIELD => 'ContentType',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Ticket content type'), $args{'OPERATOR'},
+ $args{'VALUE'}, ),
+ );
}
-# }}}
-# {{{ sub LimitCc
-=head2 LimitCC
-It\'s like LimitWatcher, but it presets TYPE to Cc
+
+=head2 LimitOwner
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=.
+VALUE is a user id.
=cut
-sub LimitCc {
+sub LimitOwner {
my $self = shift;
- $self->LimitWatcher(TYPE=> 'Cc', @_);
+ my %args = (
+ OPERATOR => '=',
+ @_
+ );
+
+ my $owner = RT::User->new( $self->CurrentUser );
+ $owner->Load( $args{'VALUE'} );
+
+ # FIXME: check for a valid $owner
+ $self->Limit(
+ FIELD => 'Owner',
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ DESCRIPTION => join( ' ',
+ $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
+ );
+
}
-# }}}
-# {{{ sub LimitAdminCc
-=head2 LimitAdminCc
-It\'s like LimitWatcher, but it presets TYPE to AdminCc
+=head2 LimitWatcher
+
+ Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
+ OPERATOR is one of =, LIKE, NOT LIKE or !=.
+ VALUE is a value to match the ticket's watcher email addresses against
+ TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
+
=cut
-
-sub LimitAdminCc {
+
+sub LimitWatcher {
my $self = shift;
- $self->LimitWatcher(TYPE=> 'AdminCc', @_);
+ my %args = (
+ OPERATOR => '=',
+ VALUE => undef,
+ TYPE => undef,
+ @_
+ );
+
+ #build us up a description
+ my ( $watcher_type, $desc );
+ if ( $args{'TYPE'} ) {
+ $watcher_type = $args{'TYPE'};
+ }
+ else {
+ $watcher_type = "Watcher";
+ }
+
+ $self->Limit(
+ FIELD => $watcher_type,
+ VALUE => $args{'VALUE'},
+ OPERATOR => $args{'OPERATOR'},
+ TYPE => $args{'TYPE'},
+ DESCRIPTION => join( ' ',
+ $self->loc($watcher_type),
+ $args{'OPERATOR'}, $args{'VALUE'}, ),
+ );
}
-# }}}
-# }}}
-# }}}
-# {{{ Limiting based on links
-# {{{ LimitLinkedTo
=head2 LimitLinkedTo
LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
+
+TYPE = { RefersTo, MemberOf, DependsOn }
TARGET is the id or URI of the TARGET of the link
-(TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
=cut
sub LimitLinkedTo {
my $self = shift;
- my %args = (
- TICKET => undef,
- TARGET => undef,
- TYPE => undef,
- @_);
-
-
- $self->Limit( FIELD => 'LinkedTo',
- BASE => undef,
- TARGET => ($args{'TARGET'} || $args{'TICKET'}),
- TYPE => $args{'TYPE'},
- DESCRIPTION => "Tickets ".$args{'TYPE'}." by ".($args{'TARGET'} || $args{'TICKET'})
- );
+ my %args = (
+ TARGET => undef,
+ TYPE => undef,
+ OPERATOR => '=',
+ @_
+ );
+
+ $self->Limit(
+ FIELD => 'LinkedTo',
+ BASE => undef,
+ TARGET => $args{'TARGET'},
+ TYPE => $args{'TYPE'},
+ DESCRIPTION => $self->loc(
+ "Tickets [_1] by [_2]",
+ $self->loc( $args{'TYPE'} ),
+ $args{'TARGET'}
+ ),
+ OPERATOR => $args{'OPERATOR'},
+ );
}
-# }}}
-
-# {{{ LimitLinkedFrom
=head2 LimitLinkedFrom
LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
BASE is the id or URI of the BASE of the link
-(BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
-
=cut
sub LimitLinkedFrom {
my $self = shift;
- my %args = ( BASE => undef,
- TICKET => undef,
- TYPE => undef,
- @_);
-
-
- $self->Limit( FIELD => 'LinkedTo',
- TARGET => undef,
- BASE => ($args{'BASE'} || $args{'TICKET'}),
- TYPE => $args{'TYPE'},
- DESCRIPTION => "Tickets " .($args{'BASE'} || $args{'TICKET'}) ." ".$args{'TYPE'}
- );
+ my %args = (
+ BASE => undef,
+ TYPE => undef,
+ OPERATOR => '=',
+ @_
+ );
+
+ # translate RT2 From/To naming to RT3 TicketSQL naming
+ my %fromToMap = qw(DependsOn DependentOn
+ MemberOf HasMember
+ RefersTo ReferredToBy);
+
+ my $type = $args{'TYPE'};
+ $type = $fromToMap{$type} if exists( $fromToMap{$type} );
+
+ $self->Limit(
+ FIELD => 'LinkedTo',
+ TARGET => undef,
+ BASE => $args{'BASE'},
+ TYPE => $type,
+ DESCRIPTION => $self->loc(
+ "Tickets [_1] [_2]",
+ $self->loc( $args{'TYPE'} ),
+ $args{'BASE'},
+ ),
+ OPERATOR => $args{'OPERATOR'},
+ );
}
-# }}}
-
-# {{{ LimitMemberOf
sub LimitMemberOf {
- my $self = shift;
+ my $self = shift;
my $ticket_id = shift;
- $self->LimitLinkedTo ( TARGET=> "$ticket_id",
- TYPE => 'MemberOf',
- );
-
+ return $self->LimitLinkedTo(
+ @_,
+ TARGET => $ticket_id,
+ TYPE => 'MemberOf',
+ );
}
-# }}}
-# {{{ LimitHasMember
+
sub LimitHasMember {
- my $self = shift;
- my $ticket_id =shift;
- $self->LimitLinkedFrom ( BASE => "$ticket_id",
- TYPE => 'MemberOf',
- );
-
+ my $self = shift;
+ my $ticket_id = shift;
+ return $self->LimitLinkedFrom(
+ @_,
+ BASE => "$ticket_id",
+ TYPE => 'HasMember',
+ );
+
}
-# }}}
-# {{{ LimitDependsOn
+
sub LimitDependsOn {
- my $self = shift;
+ my $self = shift;
my $ticket_id = shift;
- $self->LimitLinkedTo ( TARGET => "$ticket_id",
- TYPE => 'DependsOn',
- );
-
+ return $self->LimitLinkedTo(
+ @_,
+ TARGET => $ticket_id,
+ TYPE => 'DependsOn',
+ );
+
}
-# }}}
-# {{{ LimitDependedOnBy
sub LimitDependedOnBy {
- my $self = shift;
+ my $self = shift;
my $ticket_id = shift;
- $self->LimitLinkedFrom ( BASE => "$ticket_id",
- TYPE => 'DependsOn',
- );
-
-}
+ return $self->LimitLinkedFrom(
+ @_,
+ BASE => $ticket_id,
+ TYPE => 'DependentOn',
+ );
-# }}}
+}
-# {{{ LimitRefersTo
sub LimitRefersTo {
- my $self = shift;
+ my $self = shift;
my $ticket_id = shift;
- $self->LimitLinkedTo ( TARGET => "$ticket_id",
- TYPE => 'RefersTo',
- );
-
+ return $self->LimitLinkedTo(
+ @_,
+ TARGET => $ticket_id,
+ TYPE => 'RefersTo',
+ );
+
}
-# }}}
-# {{{ LimitReferredToBy
sub LimitReferredToBy {
- my $self = shift;
+ my $self = shift;
my $ticket_id = shift;
- $self->LimitLinkedFrom ( BASE=> "$ticket_id",
- TYPE => 'RefersTo',
- );
-
+ return $self->LimitLinkedFrom(
+ @_,
+ BASE => $ticket_id,
+ TYPE => 'ReferredToBy',
+ );
}
-# }}}
-# }}}
-# {{{ limit based on ticket date attribtes
-# {{{ sub LimitDate
=head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
Takes a paramhash with the fields FIELD OPERATOR and VALUE.
-OPERATOR is one of > or <
+OPERATOR is one of > or <
VALUE is a date and time in ISO format in GMT
FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
sub LimitDate {
my $self = shift;
my %args = (
- FIELD => undef,
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
+ FIELD => undef,
+ VALUE => undef,
+ OPERATOR => undef,
- @_);
+ @_
+ );
#Set the description if we didn't get handed it above
- unless ($args{'DESCRIPTION'} ) {
- $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+ unless ( $args{'DESCRIPTION'} ) {
+ $args{'DESCRIPTION'} = $args{'FIELD'} . " "
+ . $args{'OPERATOR'} . " "
+ . $args{'VALUE'} . " GMT";
}
- $self->Limit (%args);
+ $self->Limit(%args);
}
-# }}}
-
-
-
sub LimitCreated {
my $self = shift;
- $self->LimitDate( FIELD => 'Created', @_);
+ $self->LimitDate( FIELD => 'Created', @_ );
}
+
sub LimitDue {
my $self = shift;
- $self->LimitDate( FIELD => 'Due', @_);
+ $self->LimitDate( FIELD => 'Due', @_ );
}
+
sub LimitStarts {
my $self = shift;
- $self->LimitDate( FIELD => 'Starts', @_);
+ $self->LimitDate( FIELD => 'Starts', @_ );
}
+
sub LimitStarted {
my $self = shift;
- $self->LimitDate( FIELD => 'Started', @_);
+ $self->LimitDate( FIELD => 'Started', @_ );
}
-sub LimitResolved {
+
+sub LimitResolved {
my $self = shift;
- $self->LimitDate( FIELD => 'Resolved', @_);
+ $self->LimitDate( FIELD => 'Resolved', @_ );
}
+
sub LimitTold {
my $self = shift;
- $self->LimitDate( FIELD => 'Told', @_);
+ $self->LimitDate( FIELD => 'Told', @_ );
}
+
sub LimitLastUpdated {
my $self = shift;
- $self->LimitDate( FIELD => 'LastUpdated', @_);
+ $self->LimitDate( FIELD => 'LastUpdated', @_ );
}
+
#
-# {{{ sub LimitTransactionDate
=head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
Takes a paramhash with the fields FIELD OPERATOR and VALUE.
-OPERATOR is one of > or <
+OPERATOR is one of > or <
VALUE is a date and time in ISO format in GMT
sub LimitTransactionDate {
my $self = shift;
my %args = (
- FIELD => 'TransactionDate',
- VALUE => $args{'VALUE'},
- OPERATOR => $args{'OPERATOR'},
+ FIELD => 'TransactionDate',
+ VALUE => undef,
+ OPERATOR => undef,
+
+ @_
+ );
- @_);
+ # <20021217042756.GK28744@pallas.fsck.com>
+ # "Kill It" - Jesse.
#Set the description if we didn't get handed it above
- unless ($args{'DESCRIPTION'} ) {
- $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+ unless ( $args{'DESCRIPTION'} ) {
+ $args{'DESCRIPTION'} = $args{'FIELD'} . " "
+ . $args{'OPERATOR'} . " "
+ . $args{'VALUE'} . " GMT";
}
- $self->Limit (%args);
+ $self->Limit(%args);
}
-# }}}
-# }}}
-# {{{ sub LimitKeyword
-=head2 LimitKeyword
+=head2 LimitCustomField
Takes a paramhash of key/value pairs with the following keys:
=over 4
-=item KEYWORDSELECT - KeywordSelect id
+=item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
-=item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=')
+=item OPERATOR - The usual Limit operators
-=item KEYWORD - Keyword id
+=item VALUE - The value to compare against
=back
=cut
-sub LimitKeyword {
+sub LimitCustomField {
my $self = shift;
- my %args = ( KEYWORD => undef,
- KEYWORDSELECT => undef,
- OPERATOR => '=',
- DESCRIPTION => undef,
- FIELD => 'Keyword',
- QUOTEVALUE => 1,
- @_
- );
-
- use RT::KeywordSelect;
- my $KeywordSelect = RT::KeywordSelect->new($self->CurrentUser);
- $KeywordSelect->Load($args{KEYWORDSELECT});
-
-
- # Below, We're checking to see whether the keyword we're searching for
- # is null or not.
- # This could probably be rewritten to be easier to read and understand
+ my %args = (
+ VALUE => undef,
+ CUSTOMFIELD => undef,
+ OPERATOR => '=',
+ DESCRIPTION => undef,
+ FIELD => 'CustomFieldValue',
+ QUOTEVALUE => 1,
+ @_
+ );
+
+ my $CF = RT::CustomField->new( $self->CurrentUser );
+ if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
+ $CF->Load( $args{CUSTOMFIELD} );
+ }
+ else {
+ $CF->LoadByNameAndQueue(
+ Name => $args{CUSTOMFIELD},
+ Queue => $args{QUEUE}
+ );
+ $args{CUSTOMFIELD} = $CF->Id;
+ }
-
#If we are looking to compare with a null value.
- if ($args{'OPERATOR'} =~ /is/i) {
- if ($args{'OPERATOR'} =~ /^is$/i) {
- $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has no value";
- }
- elsif ($args{'OPERATOR'} =~ /^is not$/i) {
- $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has a value";
- }
- }
- # if we're not looking to compare with a null value
- else {
- use RT::Keyword;
- my $Keyword = RT::Keyword->new($self->CurrentUser);
- $Keyword->Load($args{KEYWORD});
- $args{'DESCRIPTION'} ||= "Keyword Selection " . $KeywordSelect->Name. " $args{OPERATOR} ". $Keyword->Name;
+ if ( $args{'OPERATOR'} =~ /^is$/i ) {
+ $args{'DESCRIPTION'}
+ ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
}
-
- $args{SingleValued} = $KeywordSelect->Single();
-
-
- my $index = $self->_NextIndex;
- %{$self->{'TicketRestrictions'}{$index}} = %args;
-
+ elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
+ $args{'DESCRIPTION'}
+ ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
+ }
+
+ # if we're not looking to compare with a null value
+ else {
+ $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
+ $CF->Name, $args{OPERATOR}, $args{VALUE} );
+ }
+
+ if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
+ my $QueueObj = RT::Queue->new( $self->CurrentUser );
+ $QueueObj->Load( $args{'QUEUE'} );
+ $args{'QUEUE'} = $QueueObj->Id;
+ }
+ delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
+
+ my @rest;
+ @rest = ( ENTRYAGGREGATOR => 'AND' )
+ if ( $CF->Type eq 'SelectMultiple' );
+
+ $self->Limit(
+ VALUE => $args{VALUE},
+ FIELD => "CF"
+ .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
+ .".{" . $CF->Name . "}",
+ OPERATOR => $args{OPERATOR},
+ CUSTOMFIELD => 1,
+ @rest,
+ );
+
$self->{'RecalcTicketLimits'} = 1;
- return ($index);
}
-# }}}
-# {{{ sub _NextIndex
=head2 _NextIndex
sub _NextIndex {
my $self = shift;
- return ($self->{'restriction_index'}++);
+ return ( $self->{'restriction_index'}++ );
}
-# }}}
-# }}}
-# {{{ Core bits to make this a DBIx::SearchBuilder object
-# {{{ sub _Init
-sub _Init {
+
+sub _Init {
my $self = shift;
- $self->{'table'} = "Tickets";
- $self->{'RecalcTicketLimits'} = 1;
+ $self->{'table'} = "Tickets";
+ $self->{'RecalcTicketLimits'} = 1;
$self->{'looking_at_effective_id'} = 0;
- $self->{'restriction_index'} =1;
- $self->{'primary_key'} = "id";
+ $self->{'looking_at_type'} = 0;
+ $self->{'restriction_index'} = 1;
+ $self->{'primary_key'} = "id";
+ delete $self->{'items_array'};
+ delete $self->{'item_map'};
+ delete $self->{'columns_to_display'};
$self->SUPER::_Init(@_);
+ $self->_InitSQL;
+
}
-# }}}
-# {{{ sub NewItem
-sub NewItem {
- my $self = shift;
- return(RT::Ticket->new($self->CurrentUser));
+sub Count {
+ my $self = shift;
+ $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+ return ( $self->SUPER::Count() );
}
-# }}}
-# {{{ sub Count
-sub Count {
- my $self = shift;
- $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
- return($self->SUPER::Count());
+
+sub CountAll {
+ my $self = shift;
+ $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+ return ( $self->SUPER::CountAll() );
}
-# }}}
-# {{{ sub ItemsArrayRef
+
=head2 ItemsArrayRef
sub ItemsArrayRef {
my $self = shift;
- my @items;
-
+
+ return $self->{'items_array'} if $self->{'items_array'};
+
my $placeholder = $self->_ItemsCounter;
$self->GotoFirstItem();
- while (my $item = $self->Next) {
- push (@items, $item);
+ while ( my $item = $self->Next ) {
+ push( @{ $self->{'items_array'} }, $item );
}
-
$self->GotoItem($placeholder);
- return(\@items);
+ $self->{'items_array'}
+ = $self->ItemsOrderBy( $self->{'items_array'} );
+
+ return $self->{'items_array'};
}
-# }}}
-# {{{ sub Next
+sub ItemsArrayRefWindow {
+ my $self = shift;
+ my $window = shift;
+
+ my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
+
+ $self->RowsPerPage( $window );
+ $self->FirstRow(1);
+ $self->GotoFirstItem;
+
+ my @res;
+ while ( my $item = $self->Next ) {
+ push @res, $item;
+ }
+
+ $self->RowsPerPage( $old[1] );
+ $self->FirstRow( $old[2] );
+ $self->GotoItem( $old[0] );
+
+ return \@res;
+}
+
+
sub Next {
- my $self = shift;
-
- $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
+ my $self = shift;
+
+ $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+
+ my $Ticket = $self->SUPER::Next;
+ return $Ticket unless $Ticket;
+
+ if ( $Ticket->__Value('Status') eq 'deleted'
+ && !$self->{'allow_deleted_search'} )
+ {
+ return $self->Next;
+ }
+ elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
+ # if we found a ticket with this option enabled then
+ # all tickets we found are ACLed, cache this fact
+ my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
+ $RT::Principal::_ACL_CACHE->set( $key => 1 );
+ return $Ticket;
+ }
+ elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+ # has rights
+ return $Ticket;
+ }
+ else {
+ # If the user doesn't have the right to show this ticket
+ return $self->Next;
+ }
+}
+
+sub _DoSearch {
+ my $self = shift;
+ $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+ return $self->SUPER::_DoSearch( @_ );
+}
+
+sub _DoCount {
+ my $self = shift;
+ $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+ return $self->SUPER::_DoCount( @_ );
+}
+
+sub _RolesCanSee {
+ my $self = shift;
+
+ my $cache_key = 'RolesHasRight;:;ShowTicket';
+
+ if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+ return %$cached;
+ }
+
+ my $ACL = RT::ACL->new( RT->SystemUser );
+ $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+ $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
+ my $principal_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+
+ my %res = ();
+ foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+ my $role = $ACE->__Value('PrincipalType');
+ my $type = $ACE->__Value('ObjectType');
+ if ( $type eq 'RT::System' ) {
+ $res{ $role } = 1;
+ }
+ elsif ( $type eq 'RT::Queue' ) {
+ next if $res{ $role } && !ref $res{ $role };
+ push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
+ }
+ else {
+ $RT::Logger->error('ShowTicket right is granted on unsupported object');
+ }
+ }
+ $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
+ return %res;
+}
+
+sub _DirectlyCanSeeIn {
+ my $self = shift;
+ my $id = $self->CurrentUser->id;
+
+ my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
+ if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+ return @$cached;
+ }
- my $Ticket = $self->SUPER::Next();
- if ((defined($Ticket)) and (ref($Ticket))) {
+ my $ACL = RT::ACL->new( RT->SystemUser );
+ $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+ my $principal_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+ my $cgm_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ );
+ $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+ $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+
+ my @res = ();
+ foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+ my $type = $ACE->__Value('ObjectType');
+ if ( $type eq 'RT::System' ) {
+ # If user is direct member of a group that has the right
+ # on the system then he can see any ticket
+ $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
+ return (-1);
+ }
+ elsif ( $type eq 'RT::Queue' ) {
+ push @res, $ACE->__Value('ObjectId');
+ }
+ else {
+ $RT::Logger->error('ShowTicket right is granted on unsupported object');
+ }
+ }
+ $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
+ return @res;
+}
- #Make sure we _never_ show dead tickets
- #TODO we should be doing this in the where clause.
- #but you can't do multiple clauses on the same field just yet :/
+sub CurrentUserCanSee {
+ my $self = shift;
+ return if $self->{'_sql_current_user_can_see_applied'};
+
+ return $self->{'_sql_current_user_can_see_applied'} = 1
+ if $self->CurrentUser->UserObj->HasRight(
+ Right => 'SuperUser', Object => $RT::System
+ );
+
+ my $id = $self->CurrentUser->id;
+
+ # directly can see in all queues then we have nothing to do
+ my @direct_queues = $self->_DirectlyCanSeeIn;
+ return $self->{'_sql_current_user_can_see_applied'} = 1
+ if @direct_queues && $direct_queues[0] == -1;
+
+ my %roles = $self->_RolesCanSee;
+ {
+ my %skip = map { $_ => 1 } @direct_queues;
+ foreach my $role ( keys %roles ) {
+ next unless ref $roles{ $role };
+
+ my @queues = grep !$skip{$_}, @{ $roles{ $role } };
+ if ( @queues ) {
+ $roles{ $role } = \@queues;
+ } else {
+ delete $roles{ $role };
+ }
+ }
+ }
- if ($Ticket->Status eq 'dead') {
- return($self->Next());
- }
- elsif ($Ticket->CurrentUserHasRight('ShowTicket')) {
- return($Ticket);
- }
+# there is no global watchers, only queues and tickes, if at
+# some point we will add global roles then it's gonna blow
+# the idea here is that if the right is set globaly for a role
+# and user plays this role for a queue directly not a ticket
+# then we have to check in advance
+ if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
+
+ my $groups = RT::Groups->new( RT->SystemUser );
+ $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
+ foreach ( @tmp ) {
+ $groups->Limit( FIELD => 'Type', VALUE => $_ );
+ }
+ my $principal_alias = $groups->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+ my $cgm_alias = $groups->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ );
+ $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+ $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+ while ( my $group = $groups->Next ) {
+ push @direct_queues, $group->Instance;
+ }
+ }
- #If the user doesn't have the right to show this ticket
- else {
- return($self->Next());
- }
- }
- #if there never was any ticket
- else {
- return(undef);
- }
+ unless ( @direct_queues || keys %roles ) {
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => 'main',
+ FIELD => 'id',
+ VALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ return $self->{'_sql_current_user_can_see_applied'} = 1;
+ }
+ {
+ my $join_roles = keys %roles;
+ $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
+ my ($role_group_alias, $cgm_alias);
+ if ( $join_roles ) {
+ $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
+ $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
+ $self->SUPER::Limit(
+ LEFTJOIN => $cgm_alias,
+ FIELD => 'MemberId',
+ OPERATOR => '=',
+ VALUE => $id,
+ );
+ }
+ my $limit_queues = sub {
+ my $ea = shift;
+ my @queues = @_;
+
+ return unless @queues;
+ if ( @queues == 1 ) {
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => 'main',
+ FIELD => 'Queue',
+ VALUE => $_[0],
+ ENTRYAGGREGATOR => $ea,
+ );
+ } else {
+ $self->SUPER::_OpenParen('ACL');
+ foreach my $q ( @queues ) {
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => 'main',
+ FIELD => 'Queue',
+ VALUE => $q,
+ ENTRYAGGREGATOR => $ea,
+ );
+ $ea = 'OR';
+ }
+ $self->SUPER::_CloseParen('ACL');
+ }
+ return 1;
+ };
+
+ $self->SUPER::_OpenParen('ACL');
+ my $ea = 'AND';
+ $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
+ while ( my ($role, $queues) = each %roles ) {
+ $self->SUPER::_OpenParen('ACL');
+ if ( $role eq 'Owner' ) {
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ FIELD => 'Owner',
+ VALUE => $id,
+ ENTRYAGGREGATOR => $ea,
+ );
+ }
+ else {
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => $cgm_alias,
+ FIELD => 'MemberId',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => $ea,
+ );
+ $self->SUPER::Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => $role_group_alias,
+ FIELD => 'Type',
+ VALUE => $role,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ }
+ $limit_queues->( 'AND', @$queues ) if ref $queues;
+ $ea = 'OR' if $ea eq 'AND';
+ $self->SUPER::_CloseParen('ACL');
+ }
+ $self->SUPER::_CloseParen('ACL');
+ }
+ return $self->{'_sql_current_user_can_see_applied'} = 1;
}
-# }}}
-# }}}
-# {{{ Deal with storing and restoring restrictions
-# {{{ sub LoadRestrictions
+
=head2 LoadRestrictions
=cut
-# }}}
-# {{{ sub DescribeRestrictions
=head2 DescribeRestrictions
takes nothing.
-Returns a hash keyed by restriction id.
+Returns a hash keyed by restriction id.
Each element of the hash is currently a one element hash that contains DESCRIPTION which
is a description of the purpose of that TicketRestriction
=cut
-sub DescribeRestrictions {
+sub DescribeRestrictions {
my $self = shift;
-
- my ($row, %listing);
-
- foreach $row (keys %{$self->{'TicketRestrictions'}}) {
- $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
+
+ my %listing;
+
+ foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+ $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
}
return (%listing);
}
-# }}}
-# {{{ sub RestrictionValues
+
=head2 RestrictionValues FIELD
=cut
sub RestrictionValues {
- my $self = shift;
+ my $self = shift;
my $field = shift;
- map $self->{'TicketRestrictions'}{$_}{'VALUE'},
- grep {
- $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
- && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
- }
- keys %{$self->{'TicketRestrictions'}};
+ map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
+ $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
+ && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
+ }
+ keys %{ $self->{'TicketRestrictions'} };
}
-# }}}
-# {{{ sub ClearRestrictions
=head2 ClearRestrictions
Removes all restrictions irretrievably
=cut
-
+
sub ClearRestrictions {
my $self = shift;
delete $self->{'TicketRestrictions'};
$self->{'looking_at_effective_id'} = 0;
- $self->{'RecalcTicketLimits'} =1;
+ $self->{'looking_at_type'} = 0;
+ $self->{'RecalcTicketLimits'} = 1;
}
-# }}}
-# {{{ sub DeleteRestriction
=head2 DeleteRestriction
=cut
-
sub DeleteRestriction {
my $self = shift;
- my $row = shift;
+ my $row = shift;
delete $self->{'TicketRestrictions'}{$row};
-
+
$self->{'RecalcTicketLimits'} = 1;
+
#make the underlying easysearch object forget all its preconceptions
}
-# }}}
-# {{{ sub _ProcessRestrictions
-sub _ProcessRestrictions {
+# Convert a set of oldstyle SB Restrictions to Clauses for RQL
+
+sub _RestrictionsToClauses {
my $self = shift;
- #Need to clean the EasySearch slate because it makes things too sticky
- $self->CleanSlate();
+ my %clause;
+ foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+ my $restriction = $self->{'TicketRestrictions'}{$row};
- #Blow away ticket aliases since we'll need to regenerate them for a new search
- delete $self->{'TicketAliases'};
- delete $self->{KeywordsAliases};
+ # We need to reimplement the subclause aggregation that SearchBuilder does.
+ # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
+ # Then SB AND's the different Subclauses together.
- my $row;
-
- foreach $row (keys %{$self->{'TicketRestrictions'}}) {
- my $restriction = $self->{'TicketRestrictions'}{$row};
- # {{{ if it's an int
-
- if ($TYPES{$restriction->{'FIELD'}} eq 'INT' ) {
- if ($restriction->{'OPERATOR'} =~ /^(=|!=|>|<|>=|<=)$/) {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => $restriction->{'OPERATOR'},
- VALUE => $restriction->{'VALUE'},
- );
- }
- }
- # }}}
- # {{{ if it's an enum
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'ENUM') {
-
- if ($restriction->{'OPERATOR'} eq '=') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'OR',
- OPERATOR => '=',
- VALUE => $restriction->{'VALUE'},
- );
- }
- elsif ($restriction->{'OPERATOR'} eq '!=') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => '!=',
- VALUE => $restriction->{'VALUE'},
- );
- }
-
- }
- # }}}
- # {{{ if it's a date
-
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'DATE') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => $restriction->{'OPERATOR'},
- VALUE => $restriction->{'VALUE'},
- );
- }
- # }}}
- # {{{ if it's a string
-
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'STRING') {
-
- if ($restriction->{'OPERATOR'} eq '=') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'OR',
- OPERATOR => '=',
- VALUE => $restriction->{'VALUE'},
- CASESENSITIVE => 0
- );
- }
- elsif ($restriction->{'OPERATOR'} eq '!=') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => '!=',
- VALUE => $restriction->{'VALUE'},
- CASESENSITIVE => 0
- );
- }
- elsif ($restriction->{'OPERATOR'} eq 'LIKE') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => 'LIKE',
- VALUE => $restriction->{'VALUE'},
- CASESENSITIVE => 0
- );
- }
- elsif ($restriction->{'OPERATOR'} eq 'NOT LIKE') {
- $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => 'NOT LIKE',
- VALUE => $restriction->{'VALUE'},
- CASESENSITIVE => 0
- );
- }
- }
-
- # }}}
- # {{{ if it's Transaction content that we're hunting for
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSFIELD') {
-
- #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.
- #We put them in TicketAliases so that they get nuked when we redo the join.
-
- unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
- $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
- }
- unless (defined $self->{'TicketAliases'}{'TransFieldAttachAlias'}){
- $self->{'TicketAliases'}{'TransFieldAttachAlias'} = $self->NewAlias('Attachments');
-
- }
- #Join transactions to attachments
- $self->Join( ALIAS1 => $self->{'TicketAliases'}{'TransFieldAttachAlias'},
- FIELD1 => 'TransactionId',
- ALIAS2 => $self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2=> 'id');
-
- #Join transactions to tickets
- $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
- ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
-
- #Search for the right field
- $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAttachAlias'},
- ENTRYAGGREGATOR => 'AND',
- FIELD => $restriction->{'FIELD'},
- OPERATOR => $restriction->{'OPERATOR'} ,
- VALUE => $restriction->{'VALUE'},
- CASESENSITIVE => 0
- );
-
-
- }
-
- # }}}
- # {{{ if it's a Transaction date that we're hunting for
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSDATE') {
-
- #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.
- #We put them in TicketAliases so that they get nuked when we redo the join.
-
- unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
- $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
- }
-
- #Join transactions to tickets
- $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
- ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
-
- #Search for the right field
- $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAlias'},
- ENTRYAGGREGATOR => 'AND',
- FIELD => 'Created',
- OPERATOR => $restriction->{'OPERATOR'} ,
- VALUE => $restriction->{'VALUE'} );
- }
-
- # }}}
- # {{{ if it's a relationship that we're hunting for
-
- # Takes FIELD: which is something like "LinkedTo"
- # takes TARGET or BASE which is the TARGET or BASE id that we're searching for
- # takes TYPE which is the type of link we're looking for.
-
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'LINKFIELD') {
-
-
- my $LinkAlias = $self->NewAlias ('Links');
-
-
- #Make sure we get the right type of link, if we're restricting it
- if ($restriction->{'TYPE'}) {
- $self->SUPER::Limit(ALIAS => $LinkAlias,
- ENTRYAGGREGATOR => 'AND',
- FIELD => 'Type',
- OPERATOR => '=',
- VALUE => $restriction->{'TYPE'} );
- }
-
- #If we're trying to limit it to things that are target of
- if ($restriction->{'TARGET'}) {
-
-
- # If the TARGET is an integer that means that we want to look at the LocalTarget
- # field. otherwise, we want to look at the "Target" field
-
- my ($matchfield);
- if ($restriction->{'TARGET'} =~/^(\d+)$/) {
- $matchfield = "LocalTarget";
- }
- else {
- $matchfield = "Target";
- }
-
- $self->SUPER::Limit(ALIAS => $LinkAlias,
- ENTRYAGGREGATOR => 'AND',
- FIELD => $matchfield,
- OPERATOR => '=',
- VALUE => $restriction->{'TARGET'} );
-
-
- #If we're searching on target, join the base to ticket.id
- $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
- ALIAS2 => $LinkAlias,
- FIELD2 => 'LocalBase');
-
-
-
-
- }
- #If we're trying to limit it to things that are base of
- elsif ($restriction->{'BASE'}) {
-
-
- # If we're trying to match a numeric link, we want to look at LocalBase,
- # otherwise we want to look at "Base"
-
- my ($matchfield);
- if ($restriction->{'BASE'} =~/^(\d+)$/) {
- $matchfield = "LocalBase";
- }
- else {
- $matchfield = "Base";
- }
-
-
- $self->SUPER::Limit(ALIAS => $LinkAlias,
- ENTRYAGGREGATOR => 'AND',
- FIELD => $matchfield,
- OPERATOR => '=',
- VALUE => $restriction->{'BASE'} );
-
- #If we're searching on base, join the target to ticket.id
- $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
- ALIAS2 => $LinkAlias,
- FIELD2 => 'LocalTarget');
-
- }
-
- }
-
- # }}}
- # {{{ if it's a watcher that we're hunting for
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'WATCHERFIELD') {
-
- my $Watch = $self->NewAlias('Watchers');
-
- #Join watchers to users
- my $User = $self->Join( TYPE => 'left',
- ALIAS1 => $Watch,
- FIELD1 => 'Owner',
- TABLE2 => 'Users',
- FIELD2 => 'id',
- );
-
- #Join Ticket to watchers
- $self->Join( ALIAS1 => 'main', FIELD1 => 'id',
- ALIAS2 => $Watch, FIELD2 => 'Value');
-
-
- #Make sure we're only talking about ticket watchers
- $self->SUPER::Limit( ALIAS => $Watch,
- FIELD => 'Scope',
- VALUE => 'Ticket',
- OPERATOR => '=');
-
-
- # Find email address watchers
- $self->SUPER::Limit( SUBCLAUSE => 'WatcherEmailAddress',
- ALIAS => $Watch,
- FIELD => 'Email',
- ENTRYAGGREGATOR => 'OR',
- VALUE => $restriction->{'VALUE'},
- OPERATOR => $restriction->{'OPERATOR'},
- CASESENSITIVE => 0
- );
-
-
-
- #Find user watchers
- $self->SUPER::Limit(
- SUBCLAUSE => 'WatcherEmailAddress',
- ALIAS => $User,
- FIELD => 'EmailAddress',
- ENTRYAGGREGATOR => 'OR',
- VALUE => $restriction->{'VALUE'},
- OPERATOR => $restriction->{'OPERATOR'},
- CASESENSITIVE => 0
- );
-
-
- #If we only want a specific type of watchers, then limit it to that
- if ($restriction->{'TYPE'}) {
- $self->SUPER::Limit( ALIAS => $Watch,
- FIELD => 'Type',
- ENTRYAGGREGATOR => 'OR',
- VALUE => $restriction->{'TYPE'},
- OPERATOR => '=');
- }
- }
-
- # }}}
- # {{{ if it's a keyword
- elsif ($TYPES{$restriction->{'FIELD'}} eq 'KEYWORDFIELD') {
-
- my $null_columns_ok;
-
- my $ObjKeywordsAlias;
- $ObjKeywordsAlias = $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}}
- if $restriction->{SingleValued};
- unless (defined $ObjKeywordsAlias) {
- $ObjKeywordsAlias = $self->Join(
- TYPE => 'left',
- ALIAS1 => 'main',
- FIELD1 => 'id',
- TABLE2 => 'ObjectKeywords',
- FIELD2 => 'ObjectId'
- );
- if ($restriction->{'SingleValued'}) {
- $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}}
- = $ObjKeywordsAlias;
- }
- }
-
-
- $self->SUPER::Limit(
- ALIAS => $ObjKeywordsAlias,
- FIELD => 'Keyword',
- OPERATOR => $restriction->{'OPERATOR'},
- VALUE => $restriction->{'KEYWORD'},
- QUOTEVALUE => $restriction->{'QUOTEVALUE'},
- ENTRYAGGREGATOR => 'OR',
- );
-
- if ( ($restriction->{'OPERATOR'} =~ /^IS$/i) or
- ($restriction->{'OPERATOR'} eq '!=') ) {
-
- $null_columns_ok=1;
-
- }
-
- #If we're trying to find tickets where the keyword isn't somethng, also check ones where it _IS_ null
- if ( $restriction->{'OPERATOR'} eq '!=') {
- $self->SUPER::Limit(
- ALIAS => $ObjKeywordsAlias,
- FIELD => 'Keyword',
- OPERATOR => 'IS',
- VALUE => 'NULL',
- QUOTEVALUE => 0,
- ENTRYAGGREGATOR => 'OR',
- );
- }
-
-
- $self->SUPER::Limit(LEFTJOIN => $ObjKeywordsAlias,
- FIELD => 'KeywordSelect',
- VALUE => $restriction->{'KEYWORDSELECT'},
- ENTRYAGGREGATOR => 'OR');
+ # So, we want to group things into Subclauses, convert them to
+ # SQL, and then join them with the appropriate DefaultEA.
+ # Then join each subclause group with AND.
+ my $field = $restriction->{'FIELD'};
+ my $realfield = $field; # CustomFields fake up a fieldname, so
+ # we need to figure that out
-
- $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias,
- FIELD => 'ObjectType',
- VALUE => 'Ticket',
- ENTRYAGGREGATOR => 'AND');
-
- if ($null_columns_ok) {
- $self->SUPER::Limit(ALIAS => $ObjKeywordsAlias,
- FIELD => 'ObjectType',
- OPERATOR => 'IS',
- VALUE => 'NULL',
- QUOTEVALUE => 0,
- ENTRYAGGREGATOR => 'OR');
- }
-
+ # One special case
+ # Rewrite LinkedTo meta field to the real field
+ if ( $field =~ /LinkedTo/ ) {
+ $realfield = $field = $restriction->{'TYPE'};
}
- # }}}
-
- }
-
-
- # here, we make sure we don't get any tickets that have been merged into other tickets
- # (Ticket Id == Ticket EffectiveId
- # note that we _really_ don't want to do this if we're already looking at the effectiveid
- if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
- $self->SUPER::Limit( FIELD => 'EffectiveId',
- OPERATOR => '=',
- QUOTEVALUE => 0,
- VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main.
- }
- $self->{'RecalcTicketLimits'} = 0;
-}
+ # Two special case
+ # Handle subkey fields with a different real field
+ if ( $field =~ /^(\w+)\./ ) {
+ $realfield = $1;
+ }
-# }}}
+ die "I don't know about $field yet"
+ unless ( exists $FIELD_METADATA{$realfield}
+ or $restriction->{CUSTOMFIELD} );
+
+ my $type = $FIELD_METADATA{$realfield}->[0];
+ my $op = $restriction->{'OPERATOR'};
+
+ my $value = (
+ grep {defined}
+ map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
+ )[0];
+
+ # this performs the moral equivalent of defined or/dor/C<//>,
+ # without the short circuiting.You need to use a 'defined or'
+ # type thing instead of just checking for truth values, because
+ # VALUE could be 0.(i.e. "false")
+
+ # You could also use this, but I find it less aesthetic:
+ # (although it does short circuit)
+ #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
+ # defined $restriction->{'TICKET'} ?
+ # $restriction->{TICKET} :
+ # defined $restriction->{'BASE'} ?
+ # $restriction->{BASE} :
+ # defined $restriction->{'TARGET'} ?
+ # $restriction->{TARGET} )
+
+ my $ea = $restriction->{ENTRYAGGREGATOR}
+ || $DefaultEA{$type}
+ || "AND";
+ if ( ref $ea ) {
+ die "Invalid operator $op for $field ($type)"
+ unless exists $ea->{$op};
+ $ea = $ea->{$op};
+ }
-# }}}
+ # Each CustomField should be put into a different Clause so they
+ # are ANDed together.
+ if ( $restriction->{CUSTOMFIELD} ) {
+ $realfield = $field;
+ }
-# {{{ Deal with displaying rows of the listing
+ exists $clause{$realfield} or $clause{$realfield} = [];
-#
-# Everything in this section is stub code for 2.2
-# It's not part of the API. It's not for your use
-# It's not for our use.
-#
+ # Escape Quotes
+ $field =~ s!(['\\])!\\$1!g;
+ $value =~ s!(['\\])!\\$1!g;
+ my $data = [ $ea, $type, $field, $op, $value ];
+ # here is where we store extra data, say if it's a keyword or
+ # something. (I.e. "TYPE SPECIFIC STUFF")
-# {{{ sub SetListingFormat
+ if (lc $ea eq 'none') {
+ $clause{$realfield} = [ $data ];
+ } else {
+ push @{ $clause{$realfield} }, $data;
+ }
+ }
+ return \%clause;
+}
-=head2 SetListingFormat
-Takes a single Format string as specified below. parses that format string and makes the various listing output
-things DTRT.
-=item Format strings
+=head2 _ProcessRestrictions PARAMHASH
-Format strings are made up of a chain of Elements delimited with vertical pipes (|).
-Elements of a Format string
+# The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
+# but isn't quite generic enough to move into Tickets_SQL.
+=cut
-FormatString: Element[::FormatString]
+sub _ProcessRestrictions {
+ my $self = shift;
-Element: AttributeName[;HREF=<URL>][;TITLE=<TITLE>]
+ #Blow away ticket aliases since we'll need to regenerate them for
+ #a new search
+ delete $self->{'TicketAliases'};
+ delete $self->{'items_array'};
+ delete $self->{'item_map'};
+ delete $self->{'raw_rows'};
+ delete $self->{'rows'};
+ delete $self->{'count_all'};
+
+ my $sql = $self->Query; # Violating the _SQL namespace
+ if ( !$sql || $self->{'RecalcTicketLimits'} ) {
+
+ # "Restrictions to Clauses Branch\n";
+ my $clauseRef = eval { $self->_RestrictionsToClauses; };
+ if ($@) {
+ $RT::Logger->error( "RestrictionsToClauses: " . $@ );
+ $self->FromSQL("");
+ }
+ else {
+ $sql = $self->ClausesToSQL($clauseRef);
+ $self->FromSQL($sql) if $sql;
+ }
+ }
-AttributeName Id | Subject | Status | Owner | Priority | InitialPriority | TimeWorked | TimeLeft |
-
- Keywords[;SELECT=<KeywordSelect>] |
-
- <Created|Starts|Started|Contacted|Due|Resolved>Date<AsString|AsISO|AsAge>
+ $self->{'RecalcTicketLimits'} = 0;
+}
-=cut
+=head2 _BuildItemMap
+Build up a L</ItemMap> of first/last/next/prev items, so that we can
+display search nav quickly.
+=cut
+sub _BuildItemMap {
+ my $self = shift;
-#accept a format string
+ my $window = RT->Config->Get('TicketsItemMapSize');
+ $self->{'item_map'} = {};
+ my $items = $self->ItemsArrayRefWindow( $window );
+ return unless $items && @$items;
-sub SetListingFormat {
- my $self = shift;
- my $listing_format = shift;
-
- my ($element, $attribs);
- my $i = 0;
- foreach $element (split (/::/,$listing_format)) {
- if ($element =~ /^(.*?);(.*)$/) {
- $element = $1;
- $attribs = $2;
- }
- $self->{'format_string'}->[$i]->{'Element'} = $element;
- foreach $attrib (split (/;/, $attribs)) {
- my $value = "";
- if ($attrib =~ /^(.*?)=(.*)$/) {
- $attrib = $1;
- $value = $2;
- }
- $self->{'format_string'}->[$i]->{"$attrib"} = $val;
-
- }
-
+ my $prev = 0;
+ $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
+ for ( my $i = 0; $i < @$items; $i++ ) {
+ my $item = $items->[$i];
+ my $id = $item->EffectiveId;
+ $self->{'item_map'}{$id}{'defined'} = 1;
+ $self->{'item_map'}{$id}{'prev'} = $prev;
+ $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
+ if $items->[$i+1];
+ $prev = $id;
}
- return(1);
+ $self->{'item_map'}{'last'} = $prev
+ if !$window || @$items < $window;
}
-# }}}
+=head2 ItemMap
-# {{{ sub HeaderAsHTML
-sub HeaderAsHTML {
- my $self = shift;
- my $header = "";
- my $col;
- foreach $col ( @{[ $self->{'format_string'} ]}) {
- $header .= "<TH>" . $self->_ColumnTitle($self->{'format_string'}->[$col]) . "</TH>";
-
+Returns an a map of all items found by this search. The map is a hash
+of the form:
+
+ {
+ first => <first ticket id found>,
+ last => <last ticket id found or undef>,
+
+ <ticket id> => {
+ prev => <the ticket id found before>,
+ next => <the ticket id found after>,
+ },
+ <ticket id> => {
+ prev => ...,
+ next => ...,
+ },
}
- return ($header);
-}
-# }}}
-# {{{ sub HeaderAsText
-#Print text header
-sub HeaderAsText {
- my $self = shift;
- my ($header);
-
- return ($header);
-}
-# }}}
+=cut
-# {{{ sub TicketAsHTMLRow
-#Print HTML row
-sub TicketAsHTMLRow {
+sub ItemMap {
my $self = shift;
- my $Ticket = shift;
- my ($row, $col);
- foreach $col (@{[$self->{'format_string'}]}) {
- $row .= "<TD>" . $self->_TicketColumnValue($ticket,$self->{'format_string'}->[$col]) . "</TD>";
-
- }
- return ($row);
+ $self->_BuildItemMap unless $self->{'item_map'};
+ return $self->{'item_map'};
}
-# }}}
-# {{{ sub TicketAsTextRow
-#Print text row
-sub TicketAsTextRow {
- my $self = shift;
- my ($row);
- #TODO implement
-
- return ($row);
-}
-# }}}
-# {{{ _ColumnTitle {
-sub _ColumnTitle {
+=head2 PrepForSerialization
+
+You don't want to serialize a big tickets object, as
+the {items} hash will be instantly invalid _and_ eat
+lots of space
+
+=cut
+
+sub PrepForSerialization {
my $self = shift;
-
- # Attrib is a hash
- my $attrib = shift;
-
- # return either attrib->{'TITLE'} or..
- if ($attrib->{'TITLE'}) {
- return($attrib->{'TITLE'});
- }
- # failing that, Look up the title in a hash
- else {
- #TODO create $self->{'ColumnTitles'};
- return ($self->{'ColumnTitles'}->{$attrib->{'Element'}});
- }
-
+ delete $self->{'items'};
+ delete $self->{'items_array'};
+ $self->RedoSearch();
}
-# }}}
+=head1 FLAGS
+
+RT::Tickets supports several flags which alter search behavior:
+
-# {{{ _TicketColumnValue
-sub _TicketColumnValue {
+allow_deleted_search (Otherwise never show deleted tickets in search results)
+looking_at_type (otherwise limit to type=ticket)
+
+These flags are set by calling
+
+$tickets->{'flagname'} = 1;
+
+BUG: There should be an API for this
+
+
+
+=cut
+
+
+
+=head2 NewItem
+
+Returns an empty new RT::Ticket item
+
+=cut
+
+sub NewItem {
my $self = shift;
- my $Ticket = shift;
- my $attrib = shift;
+ return(RT::Ticket->new($self->CurrentUser));
+}
+RT::Base->_ImportOverlays();
-
- my $out;
-
- SWITCH: {
- /^id/i && do {
- $out = $Ticket->id;
- last SWITCH;
- };
- /^subj/i && do {
- last SWITCH;
- $Ticket->Subject;
- };
- /^status/i && do {
- last SWITCH;
- $Ticket->Status;
- };
- /^prio/i && do {
- last SWITCH;
- $Ticket->Priority;
- };
- /^finalprio/i && do {
-
- last SWITCH;
- $Ticket->FinalPriority
- };
- /^initialprio/i && do {
-
- last SWITCH;
- $Ticket->InitialPriority;
- };
- /^timel/i && do {
-
- last SWITCH;
- $Ticket->TimeWorked;
- };
- /^timew/i && do {
-
- last SWITCH;
- $Ticket->TimeLeft;
- };
-
- /^(.*?)date(.*)$/i && do {
- my $o = $1;
- my $m = $2;
- my ($obj);
- #TODO: optimize
- $obj = $Ticket->DueObj if $o =~ /due/i;
- $obj = $Ticket->CreatedObj if $o =~ /created/i;
- $obj = $Ticket->StartsObj if $o =~ /starts/i;
- $obj = $Ticket->StartedObj if $o =~ /started/i;
- $obj = $Ticket->ToldObj if $o =~ /told/i;
- $obj = $Ticket->LastUpdatedObj if $o =~ /lastu/i;
-
- $method = 'ISO' if $m =~ /iso/i;
-
- $method = 'AsString' if $m =~ /asstring/i;
- $method = 'AgeAsString' if $m =~ /age/i;
- last SWITCH;
- $obj->$method();
-
- };
-
- /^watcher/i && do {
- last SWITCH;
- $Ticket->WatchersAsString();
- };
-
- /^requestor/i && do {
- last SWITCH;
- $Ticket->RequestorsAsString();
- };
- /^cc/i && do {
- last SWITCH;
- $Ticket->CCAsString();
- };
-
-
- /^admincc/i && do {
- last SWITCH;
- $Ticket->AdminCcAsString();
- };
-
- /^keywords/i && do {
- last SWITCH;
- #Limit it to the keyword select we're talking about, if we've got one.
- my $objkeys =$Ticket->KeywordsObj($attrib->{'SELECT'});
- $objkeys->KeywordRelativePathsAsString();
- };
-
- }
-
-}
-
-# }}}
-
-# }}}
-
-# {{{ POD
-=head2 notes
-"Enum" Things that get Is, IsNot
-
-
-"Int" Things that get Is LessThan and GreaterThan
-id
-InitialPriority
-FinalPriority
-Priority
-TimeLeft
-TimeWorked
-
-"Text" Things that get Is, Like
-Subject
-TransactionContent
-
-
-"Link" OPERATORs
-
-
-"Date" OPERATORs Is, Before, After
-
- =cut
-# }}}
1;