diff options
Diffstat (limited to 'rt/lib/RT/Tickets.pm.orig')
-rwxr-xr-x | rt/lib/RT/Tickets.pm.orig | 3892 |
1 files changed, 0 insertions, 3892 deletions
diff --git a/rt/lib/RT/Tickets.pm.orig b/rt/lib/RT/Tickets.pm.orig deleted file mode 100755 index cd5649dd9..000000000 --- a/rt/lib/RT/Tickets.pm.orig +++ /dev/null @@ -1,3892 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 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 - - RT::Tickets - A collection of Ticket objects - - -=head1 SYNOPSIS - - use RT::Tickets; - my $tickets = RT::Tickets->new($CurrentUser); - -=head1 DESCRIPTION - - A collection of RT::Tickets. - -=head1 METHODS - - -=cut - -package RT::Tickets; - -use strict; -use warnings; - - -use RT::Ticket; - -use base 'RT::SearchBuilder'; - -sub Table { 'Tickets'} - -use RT::CustomFields; -use DBIx::SearchBuilder::Unique; - -# 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 - -Returns the list of fields that lists of tickets can easily be sorted by - -=cut - -sub SortFields { - my $self = shift; - return (@SORTFIELDS); -} - - -# BEGIN SQL STUFF ********************************* - - -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 - ); -} - -=head1 Limit Helper Routines - -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 _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 = (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; -} - -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; -} - -=head2 _WatcherJoin - -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, - ); - } - - # 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; - - my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/); - $field ||= ($string =~ /^{(.*?)}$/)[0] || $string; - - my ($cf, $applied_to); - - if ( $object ) { - my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype); - $applied_to = $record_class->new( $self->CurrentUser ); - $applied_to->Load( $object ); - - if ( $applied_to->id ) { - RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')"); - } - else { - RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'"); - $object = 0; - undef $applied_to; - } - } - - if ( $field =~ /\D/ ) { - $object ||= ''; - my $cfs = RT::CustomFields->new( $self->CurrentUser ); - $cfs->Limit( FIELD => 'Name', VALUE => $field, ($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 - -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 - -Limit based on CustomFields - -Meta Data: - none - -=cut - -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; - - my $field = $rest{'SUBKEY'} || die "No field specified"; - - # 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_utf8($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; - unless ($cf_obj) { - # For those cases where we are doing a join against the - # CF name, and don't have a CFid, use Unique to make sure - # we don't show duplicate tickets. NOTE: I'm pretty sure - # this will stay mixed in for the life of the - # class/package, and not just for the life of the object. - # Potential performance issue. - require DBIx::SearchBuilder::Unique; - DBIx::SearchBuilder::Unique->import; - } - 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); -} - - - - -=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 or Name. - - -=cut - -sub LimitQueue { - my $self = shift; - 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 check for a valid queue here - - $self->Limit( - FIELD => 'Queue', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); - -} - - - -=head2 LimitStatus - -Takes a paramhash with the fields OPERATOR and VALUE. -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 => 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; -} - - - -=head2 LimitType - -Takes a paramhash with the fields OPERATOR and VALUE. -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 => join( ' ', - $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - - - -=head2 LimitSubject - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of = or !=. -VALUE is a string to search for in the subject of the ticket. - -=cut - -sub LimitSubject { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'Subject', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - -# Things that can be > < = != - - -=head2 LimitId - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of =, >, < or !=. -VALUE is a ticket Id to search for - -=cut - -sub LimitId { - my $self = shift; - my %args = ( - OPERATOR => '=', - @_ - ); - - $self->Limit( - FIELD => 'id', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => - join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - -=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 - -=cut - -sub LimitPriority { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'Priority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Priority'), - $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - -=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 - - -=cut - -sub LimitInitialPriority { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'InitialPriority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Initial Priority'), $args{'OPERATOR'}, - $args{'VALUE'}, ), - ); -} - - - -=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 - -=cut - -sub LimitFinalPriority { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'FinalPriority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Final Priority'), $args{'OPERATOR'}, - $args{'VALUE'}, ), - ); -} - - - -=head2 LimitTimeWorked - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of =, >, < or !=. -VALUE is a value to match the ticket's TimeWorked attribute - -=cut - -sub LimitTimeWorked { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'TimeWorked', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Time Worked'), - $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - -=head2 LimitTimeLeft - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of =, >, < or !=. -VALUE is a value to match the ticket's TimeLeft attribute - -=cut - -sub LimitTimeLeft { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'TimeLeft', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Time Left'), - $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - - - -=head2 LimitContent - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of =, LIKE, NOT LIKE or !=. -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 => join( ' ', - $self->loc('Ticket content'), $args{'OPERATOR'}, - $args{'VALUE'}, ), - ); -} - - - -=head2 LimitFilename - -Takes a paramhash with the fields OPERATOR and VALUE. -OPERATOR is one of =, LIKE, NOT LIKE or !=. -VALUE is a string to search for in the body of the ticket - -=cut - -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'}, ), - ); -} - - -=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 { - my $self = shift; - my %args = (@_); - $self->Limit( - FIELD => 'ContentType', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( ' ', - $self->loc('Ticket content type'), $args{'OPERATOR'}, - $args{'VALUE'}, ), - ); -} - - - - - -=head2 LimitOwner - -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 = 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(), ), - ); - -} - - - - -=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"; - } - - $self->Limit( - FIELD => $watcher_type, - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - TYPE => $args{'TYPE'}, - DESCRIPTION => join( ' ', - $self->loc($watcher_type), - $args{'OPERATOR'}, $args{'VALUE'}, ), - ); -} - - - - - - -=head2 LimitLinkedTo - -LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET -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 - -=cut - -sub LimitLinkedTo { - my $self = shift; - 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'}, - ); -} - - - -=head2 LimitLinkedFrom - -LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE -TYPE limits the sort of link we want to search on - - -BASE is the id or URI of the BASE of the link - -=cut - -sub LimitLinkedFrom { - my $self = shift; - 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'}, - ); -} - - -sub LimitMemberOf { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedTo( - @_, - TARGET => $ticket_id, - TYPE => 'MemberOf', - ); -} - - -sub LimitHasMember { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedFrom( - @_, - BASE => "$ticket_id", - TYPE => 'HasMember', - ); - -} - - - -sub LimitDependsOn { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedTo( - @_, - TARGET => $ticket_id, - TYPE => 'DependsOn', - ); - -} - - - -sub LimitDependedOnBy { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedFrom( - @_, - BASE => $ticket_id, - TYPE => 'DependentOn', - ); - -} - - - -sub LimitRefersTo { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedTo( - @_, - TARGET => $ticket_id, - TYPE => 'RefersTo', - ); - -} - - - -sub LimitReferredToBy { - my $self = shift; - my $ticket_id = shift; - return $self->LimitLinkedFrom( - @_, - BASE => $ticket_id, - TYPE => 'ReferredToBy', - ); -} - - - - - -=head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate) - -Takes a paramhash with the fields FIELD OPERATOR and VALUE. - -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 - -There are also helper functions of the form LimitFIELD that eliminate -the need to pass in a FIELD argument. - -=cut - -sub LimitDate { - my $self = shift; - my %args = ( - 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"; - } - - $self->Limit(%args); - -} - - -sub LimitCreated { - my $self = shift; - $self->LimitDate( FIELD => 'Created', @_ ); -} - -sub LimitDue { - my $self = shift; - $self->LimitDate( FIELD => 'Due', @_ ); - -} - -sub LimitStarts { - my $self = shift; - $self->LimitDate( FIELD => 'Starts', @_ ); - -} - -sub LimitStarted { - my $self = shift; - $self->LimitDate( FIELD => 'Started', @_ ); -} - -sub LimitResolved { - my $self = shift; - $self->LimitDate( FIELD => 'Resolved', @_ ); -} - -sub LimitTold { - my $self = shift; - $self->LimitDate( FIELD => 'Told', @_ ); -} - -sub LimitLastUpdated { - my $self = shift; - $self->LimitDate( FIELD => 'LastUpdated', @_ ); -} - -# - -=head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate) - -Takes a paramhash with the fields FIELD OPERATOR and VALUE. - -OPERATOR is one of > or < -VALUE is a date and time in ISO format in GMT - - -=cut - -sub LimitTransactionDate { - my $self = shift; - my %args = ( - 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"; - } - - $self->Limit(%args); - -} - - - - -=head2 LimitCustomField - -Takes a paramhash of key/value pairs with the following keys: - -=over 4 - -=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 - The usual Limit operators - -=item VALUE - The value to compare against - -=back - -=cut - -sub LimitCustomField { - my $self = shift; - 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 ) { - $args{'DESCRIPTION'} - ||= $self->loc( "Custom field [_1] has no value.", $CF->Name ); - } - 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; -} - - - -=head2 _NextIndex - -Keep track of the counter for the array of restrictions - -=cut - -sub _NextIndex { - my $self = shift; - return ( $self->{'restriction_index'}++ ); -} - - - - -sub _Init { - my $self = shift; - $self->{'table'} = "Tickets"; - $self->{'RecalcTicketLimits'} = 1; - $self->{'looking_at_effective_id'} = 0; - $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 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() ); -} - - - -=head2 ItemsArrayRef - -Returns a reference to the set of all items found in this search - -=cut - -sub ItemsArrayRef { - my $self = shift; - - return $self->{'items_array'} if $self->{'items_array'}; - - my $placeholder = $self->_ItemsCounter; - $self->GotoFirstItem(); - while ( my $item = $self->Next ) { - push( @{ $self->{'items_array'} }, $item ); - } - $self->GotoItem($placeholder); - $self->{'items_array'} - = $self->ItemsOrderBy( $self->{'items_array'} ); - - return $self->{'items_array'}; -} - -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 $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 $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; -} - -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 }; - } - } - } - -# 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; - } - } - - 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; -} - - - - - -=head2 LoadRestrictions - -LoadRestrictions takes a string which can fully populate the TicketRestrictons hash. -TODO It is not yet implemented - -=cut - - - -=head2 DescribeRestrictions - -takes nothing. -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 { - my $self = shift; - - my %listing; - - foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) { - $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'}; - } - return (%listing); -} - - - -=head2 RestrictionValues FIELD - -Takes a restriction field and returns a list of values this field is restricted -to. - -=cut - -sub RestrictionValues { - my $self = shift; - my $field = shift; - map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep { - $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field - && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "=" - } - keys %{ $self->{'TicketRestrictions'} }; -} - - - -=head2 ClearRestrictions - -Removes all restrictions irretrievably - -=cut - -sub ClearRestrictions { - my $self = shift; - delete $self->{'TicketRestrictions'}; - $self->{'looking_at_effective_id'} = 0; - $self->{'looking_at_type'} = 0; - $self->{'RecalcTicketLimits'} = 1; -} - - - -=head2 DeleteRestriction - -Takes the row Id of a restriction (From DescribeRestrictions' output, for example. -Removes that restriction from the session's limits. - -=cut - -sub DeleteRestriction { - my $self = shift; - my $row = shift; - delete $self->{'TicketRestrictions'}{$row}; - - $self->{'RecalcTicketLimits'} = 1; - - #make the underlying easysearch object forget all its preconceptions -} - - - -# Convert a set of oldstyle SB Restrictions to Clauses for RQL - -sub _RestrictionsToClauses { - my $self = shift; - - my %clause; - foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) { - my $restriction = $self->{'TicketRestrictions'}{$row}; - - # 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. - - # 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 - - # One special case - # Rewrite LinkedTo meta field to the real field - if ( $field =~ /LinkedTo/ ) { - $realfield = $field = $restriction->{'TYPE'}; - } - - # 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; - } - - exists $clause{$realfield} or $clause{$realfield} = []; - - # 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") - - if (lc $ea eq 'none') { - $clause{$realfield} = [ $data ]; - } else { - push @{ $clause{$realfield} }, $data; - } - } - return \%clause; -} - - - -=head2 _ProcessRestrictions PARAMHASH - -# The new _ProcessRestrictions is somewhat dependent on the SQL stuff, -# but isn't quite generic enough to move into Tickets_SQL. - -=cut - -sub _ProcessRestrictions { - my $self = shift; - - #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; - } - } - - $self->{'RecalcTicketLimits'} = 0; - -} - -=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; - - my $window = RT->Config->Get('TicketsItemMapSize'); - - $self->{'item_map'} = {}; - - my $items = $self->ItemsArrayRefWindow( $window ); - return unless $items && @$items; - - 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; - } - $self->{'item_map'}{'last'} = $prev - if !$window || @$items < $window; -} - -=head2 ItemMap - -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 => ..., - }, - } - -=cut - -sub ItemMap { - my $self = shift; - $self->_BuildItemMap unless $self->{'item_map'}; - return $self->{'item_map'}; -} - - - - -=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; - delete $self->{'items'}; - delete $self->{'items_array'}; - $self->RedoSearch(); -} - -=head1 FLAGS - -RT::Tickets supports several flags which alter search behavior: - - -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; - return(RT::Ticket->new($self->CurrentUser)); -} -RT::Base->_ImportOverlays(); - -1; |