[Rt-commit] rt branch, 4.0-trunk, updated. rt-4.0.1rc1-124-gcd57c38
Alex Vandiver
alexmv at bestpractical.com
Thu Jun 16 16:22:00 EDT 2011
The branch, 4.0-trunk has been updated
via cd57c38de11a14f222f6496bf466eb6c8096deea (commit)
via a2b11dbef2b465e91f77438a299b342283ddf5c9 (commit)
via 6e66dcc5f9f3a16360f31aef5162972761310187 (commit)
via 68c5df33ea845f7c3c8f0386b82bfa8036193315 (commit)
via 95a89ccdb655e284bddb9269c730c6b842d43bc6 (commit)
via dd979521fbf8f0c92228821721c89fbd3729cdaf (commit)
via 5add26c8d76d8a25626ba02ebafe0734fb34619f (commit)
via 4cdce8c2b0dca40e3ef0abfe433dc9e0627d099e (commit)
via 88b4a24e77edd29a8fdbdb0145a42437dd3041b6 (commit)
via d826a52d5662731cb9a67a9bc7bd560bbdbd7f86 (commit)
via 35457690becf4d73d09a027dabba9038948ffca0 (commit)
via 3f04608322791066bcfabfd7915991b56e545df5 (commit)
via d2055ebe2f27a38ea34dcd269978851e1a5d4ddd (commit)
via 572c5725e4c760a69a9848bacd598ca351a2f205 (commit)
via 992045b7fb51635c734a739d1f7cabe7c4f4c614 (commit)
via a251bce4a9760789c15db290b70c5d5de87912c1 (commit)
via c59d8f3dcb1c3c436de8981fddbb53f102f2e55e (commit)
via 34d6eaac2eb3df17296c0a9a6c303d82d61f6e7f (commit)
via 20b23eaa127744e5d99897724d6ae9bdac835c31 (commit)
via ef5c2c3c1eb8ef77e232f99cb5d9022af778a67c (commit)
via 01cea1c7bb1f6fdc4e11d3a8d851ef3bd23229f6 (commit)
via 3e0d834c14647b7cc4161fecc66112e6c1be5322 (commit)
via dc839582da7f3f2789bc375f02f01e618275c1c8 (commit)
via 18f490f5206f3189131c1f1aa8e1c5c1bf613660 (commit)
via f6acc6cf043408116a622acd315b59563c7631d6 (commit)
via f43d0452fb16dd444f36de1f56b619a78124f662 (commit)
via 4692fcbdcbb219ab72cd146bef140f445ad17367 (commit)
via 9c30ac738ea5cce86176d8036ee40a0aa74a4209 (commit)
via c2291f59683484d54e0a342ca920ca305040bce5 (commit)
via 06e708c6954e5a808310f2d9adeeddcf94133060 (commit)
via 7824737c6e3a43f79f374f3f310c77ba36421f0a (commit)
via 528ad20ced2a457c2989590b1e223bfe5e60e38a (commit)
via 55679544e461d916af8a2b3c458375fc5782a607 (commit)
via a9ea4c1be2206e95605e784d12f3ebed3abe0cbf (commit)
via dc5c15d849c1ef06cd7fd9341147e0b2abae222f (commit)
via 0d5d5d887dc90b50e18d9cf1ca515aa41ff6fd68 (commit)
via e31944c036e3bdb445eea20bc99fa545c879c1e7 (commit)
via 093d627de579d8175f22f9d2f1412b822af4e257 (commit)
via cf01c196b3d9a89a92988044ea1009be770da128 (commit)
via 6d19b89c55021834a7491158976df7f43f1ba726 (commit)
via 44724a010f30303f9e0592aff28587ba7b1c7f8a (commit)
via 862c1471aba2a8d55f739ae92f191973b272f91e (commit)
via d825e6a2d55200508d02fb427a12affd428c9c39 (commit)
via 103d7e0a1d9369dd8f20588da30f6dc0a93c9d15 (commit)
via a19c0edd65fdb8d7181c18a14fded91fd2baeef7 (commit)
via e43c99db83b0300a665f59b7fc8cc9fbf4fc8959 (commit)
via 78830a59954371e5c8d7d229d0171915b2b39f75 (commit)
via 236c3de77467f4498ac2fa57cb7df392e6960e7f (commit)
via 97127fb9551fb3b71243a203b904e31322f64e61 (commit)
via d7dcbef684eac6240b110fd34be2d60907d56232 (commit)
via e5e4675d9578637b259cd0aef66d75e93ca44243 (commit)
via 3ec0091143b32234ffa0faac4379eb043e8bbfac (commit)
via c65f17cb6edbbf740568fac731e0fc1e28619f8e (commit)
via c9546687690fca3dfcc5c25f352d455c4db6c783 (commit)
via f1f39bb646a68b88412fe22a15dc0ed605d9faf4 (commit)
via 61ecfd19350ec99dc0d0702f783cf300a87d1010 (commit)
via a50dd7649985d656557c94cfb46570d16ffcdb4d (commit)
via bd0ee2574eba5983275033092d278f949673c7c7 (commit)
via 08ae31080d1ddf95b30a73e6ec8af909a68f4030 (commit)
via df84d7e92eafd4317be76ae30c3a6d699a1b4502 (commit)
via 81cb90b2c987a6d5b04a83bbcd51a9180eeed071 (commit)
via efe68af7073d8c3c4e2684a8700e02d86dd7a957 (commit)
via 26c3208ea0136e6af967ea6ee4af153382ffdf86 (commit)
via 8422935fef3a45deb8d92462befc96784bf2f5e1 (commit)
via 490b434d7b028fc814f8f22bec9bbafe37e234ac (commit)
via 02ca1b08615567b8bd64984d98bbf7f31e6a0785 (commit)
via 7c1c29cebca77abadf7d1b2774a30314e0c2d231 (commit)
via 522e2bb0ecf40a32c44e92e7bdae5ea7b5dab06f (commit)
via 3f4a3eba11619e8422334cfa139c21734ae4adae (commit)
via 9b38941a77a581636568eef7c0c767ac9d0e5d09 (commit)
via 45d0d078e4b192c4d9df637286bc4ecdb563b2c3 (commit)
via f63ae396516a5709824fe9d3d569f69511217a91 (commit)
via 69a1a88429ada627c0b2314fc44640a87b0aa604 (commit)
via d40bcaae54f2f409c48f62e31e14ff7121064ce4 (commit)
via 0721bc1c2ad895ea828621750f11de711435502b (commit)
via 2aaac82a5d197f0d1f17dd194a7c6c9ce832363c (commit)
via 695d1acb59e44938ac9e18bf14a11886cfa03993 (commit)
via f7352fdfb256e048289c2a33662de5cd09cbfb42 (commit)
via 057b906e64b09ad2202fe571677ab0faa722bfcb (commit)
via bea8b97f8b860f140fd4cbfb945c8e5a4e4dce4b (commit)
via 597e7f6eeb964f1c7b9548693abfc65da013f836 (commit)
via 4d753091ab8e3d4db0d72677388aeb2e70f6ccbd (commit)
via aec41e18300346910735fc3386b08ffe7814fba5 (commit)
via b06085df1a742a266c11305d870129cab11f8d2e (commit)
via ce2c1d712ed4c52f2c8e10a91bf67e6ed9d91b60 (commit)
via a62c33a12a664c582f94ce8218fdde878eab9bfa (commit)
via 5ebd2a8214bf02278512b4d2880f512a22750c10 (commit)
via 8092f47d5d54c24fcd6e078115e878cd65c8a213 (commit)
via 9d066f9ef77ac7f8dde57b7751e5e288cdebe261 (commit)
via d44f9b476f0471f97f04c64ce603008c9ed2d3a8 (commit)
from b55e7405ce5abe243f0115278f05788e5b036344 (commit)
Summary of changes:
lib/RT/Handle.pm | 16 ++++++++++++++++
lib/RT/Interface/REST.pm | 24 ++++++++++++------------
lib/RT/Ticket.pm | 7 +++----
lib/RT/Tickets.pm | 7 +++----
share/html/Admin/Elements/EditCustomFields | 12 ++++++++----
5 files changed, 42 insertions(+), 24 deletions(-)
- Log -----------------------------------------------------------------
commit cd57c38de11a14f222f6496bf466eb6c8096deea
Merge: b55e740 a2b11db
Author: Alex Vandiver <alexmv at bestpractical.com>
Date: Thu Jun 16 16:05:01 2011 -0400
Merge branch '3.8-trunk' into 4.0-trunk
diff --cc lib/RT/Handle.pm
index 9100e25,38905de..f350beb
mode 100644,100755..100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
diff --cc lib/RT/Interface/REST.pm
index 31a4f86,1f7295d..7209150
mode 100644,100755..100644
--- a/lib/RT/Interface/REST.pm
+++ b/lib/RT/Interface/REST.pm
@@@ -45,12 -45,23 +45,13 @@@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
-
-# lib/RT/Interface/REST.pm
-#
-
package RT::Interface::REST;
use strict;
+ use warnings;
use RT;
-BEGIN {
- use base 'Exporter';
- use vars qw($VERSION @EXPORT);
-
- $VERSION = do { my @r = (q$Revision: 1.00$ =~ /\d+/g); sprintf "%d."."%02d"x$#r, @r };
-
- @EXPORT = qw(expand_list form_parse form_compose vpush vsplit);
-}
+use base 'Exporter';
+our @EXPORT = qw(expand_list form_parse form_compose vpush vsplit);
sub custom_field_spec {
my $self = shift;
@@@ -190,9 -200,9 +190,9 @@@ sub form_parse
# Returns text representing a set of forms.
sub form_compose {
my ($forms) = @_;
-- my (@text, $form);
++ my (@text);
- foreach $form (@$forms) {
+ foreach my $form (@$forms) {
my ($c, $o, $k, $e) = @$form;
my $text = "";
@@@ -204,10 -214,10 +204,10 @@@
$text .= $e;
}
elsif ($o) {
-- my (@lines, $key);
++ my (@lines);
- foreach $key (@$o) {
- my ($line, $sp, $v);
+ foreach my $key (@$o) {
- my ($line, $sp, $v);
++ my ($line, $sp);
my @values = (ref $k->{$key} eq 'ARRAY') ?
@{ $k->{$key} } :
$k->{$key};
diff --cc lib/RT/Ticket.pm
index 85228c2,edf38f0..cd6b8fe
mode 100644,100755..100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@@ -46,2977 -46,25 +46,2976 @@@
#
# END BPS TAGGED BLOCK }}}
-# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
-# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.
-#
-# !! DO NOT EDIT THIS FILE !!
-#
+=head1 SYNOPSIS
+
+ use RT::Ticket;
+ my $ticket = RT::Ticket->new($CurrentUser);
+ $ticket->Load($ticket_id);
+
+=head1 DESCRIPTION
+
+This module lets you manipulate RT\'s ticket object.
+
+
+=head1 METHODS
+
+
+=cut
+
+
+package RT::Ticket;
+
+use strict;
+use warnings;
+
+
+use RT::Queue;
+use RT::User;
+use RT::Record;
+use RT::Links;
+use RT::Date;
+use RT::CustomFields;
+use RT::Tickets;
+use RT::Transactions;
+use RT::Reminders;
+use RT::URI::fsck_com_rt;
+use RT::URI;
+use MIME::Entity;
+use Devel::GlobalDestruction;
+
+
+# A helper table for links mapping to make it easier
+# to build and parse links between tickets
+
+our %LINKTYPEMAP = (
+ MemberOf => { Type => 'MemberOf',
+ Mode => 'Target', },
+ Parents => { Type => 'MemberOf',
+ Mode => 'Target', },
+ Members => { Type => 'MemberOf',
+ Mode => 'Base', },
+ Children => { Type => 'MemberOf',
+ Mode => 'Base', },
+ HasMember => { Type => 'MemberOf',
+ Mode => 'Base', },
+ RefersTo => { Type => 'RefersTo',
+ Mode => 'Target', },
+ ReferredToBy => { Type => 'RefersTo',
+ Mode => 'Base', },
+ DependsOn => { Type => 'DependsOn',
+ Mode => 'Target', },
+ DependedOnBy => { Type => 'DependsOn',
+ Mode => 'Base', },
+ MergedInto => { Type => 'MergedInto',
+ Mode => 'Target', },
+
+);
+
+
+# A helper table for links mapping to make it easier
+# to build and parse links between tickets
+
+our %LINKDIRMAP = (
+ MemberOf => { Base => 'MemberOf',
+ Target => 'HasMember', },
+ RefersTo => { Base => 'RefersTo',
+ Target => 'ReferredToBy', },
+ DependsOn => { Base => 'DependsOn',
+ Target => 'DependedOnBy', },
+ MergedInto => { Base => 'MergedInto',
+ Target => 'MergedInto', },
+
+);
+
+
+sub LINKTYPEMAP { return \%LINKTYPEMAP }
+sub LINKDIRMAP { return \%LINKDIRMAP }
+
+our %MERGE_CACHE = (
+ effective => {},
+ merged => {},
+);
+
+
+=head2 Load
+
+Takes a single argument. This can be a ticket id, ticket alias or
+local ticket uri. If the ticket can't be loaded, returns undef.
+Otherwise, returns the ticket id.
+
+=cut
+
+sub Load {
+ my $self = shift;
+ my $id = shift;
+ $id = '' unless defined $id;
+
+ # TODO: modify this routine to look at EffectiveId and
+ # do the recursive load thing. be careful to cache all
+ # the interim tickets we try so we don't loop forever.
+
+ unless ( $id =~ /^\d+$/ ) {
+ $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
+ return (undef);
+ }
+
+ $id = $MERGE_CACHE{'effective'}{ $id }
+ if $MERGE_CACHE{'effective'}{ $id };
+
+ my ($ticketid, $msg) = $self->LoadById( $id );
+ unless ( $self->Id ) {
+ $RT::Logger->debug("$self tried to load a bogus ticket: $id");
+ return (undef);
+ }
+
+ #If we're merged, resolve the merge.
+ if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
+ $RT::Logger->debug(
+ "We found a merged ticket. "
+ . $self->id ."/". $self->EffectiveId
+ );
+ my $real_id = $self->Load( $self->EffectiveId );
+ $MERGE_CACHE{'effective'}{ $id } = $real_id;
+ return $real_id;
+ }
+
+ #Ok. we're loaded. lets get outa here.
+ return $self->Id;
+}
+
+
+
+=head2 Create (ARGS)
+
+Arguments: ARGS is a hash of named parameters. Valid parameters are:
+
+ id
+ Queue - Either a Queue object or a Queue Name
+ Requestor - A reference to a list of email addresses or RT user Names
+ Cc - A reference to a list of email addresses or Names
+ AdminCc - A reference to a list of email addresses or Names
+ SquelchMailTo - A reference to a list of email addresses -
+ who should this ticket not mail
+ Type -- The ticket\'s type. ignore this for now
+ Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
+ Subject -- A string describing the subject of the ticket
+ Priority -- an integer from 0 to 99
+ InitialPriority -- an integer from 0 to 99
+ FinalPriority -- an integer from 0 to 99
+ Status -- any valid status (Defined in RT::Queue)
+ TimeEstimated -- an integer. estimated time for this task in minutes
+ TimeWorked -- an integer. time worked so far in minutes
+ TimeLeft -- an integer. time remaining in minutes
+ Starts -- an ISO date describing the ticket\'s start date and time in GMT
+ Due -- an ISO date describing the ticket\'s due date and time in GMT
+ MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
+ CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
+
+Ticket links can be set up during create by passing the link type as a hask key and
+the ticket id to be linked to as a value (or a URI when linking to other objects).
+Multiple links of the same type can be created by passing an array ref. For example:
+
+ Parents => 45,
+ DependsOn => [ 15, 22 ],
+ RefersTo => 'http://www.bestpractical.com',
+
+Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
+C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
+C<Members> and C<Children> are aliases for C<HasMember>.
+
+Returns: TICKETID, Transaction Object, Error Message
+
+
+=cut
+
+sub Create {
+ my $self = shift;
+
+ my %args = (
+ id => undef,
+ EffectiveId => undef,
+ Queue => undef,
+ Requestor => undef,
+ Cc => undef,
+ AdminCc => undef,
+ SquelchMailTo => undef,
+ TransSquelchMailTo => undef,
+ Type => 'ticket',
+ Owner => undef,
+ Subject => '',
+ InitialPriority => undef,
+ FinalPriority => undef,
+ Priority => undef,
+ Status => undef,
+ TimeWorked => "0",
+ TimeLeft => 0,
+ TimeEstimated => 0,
+ Due => undef,
+ Starts => undef,
+ Started => undef,
+ Resolved => undef,
+ MIMEObj => undef,
+ _RecordTransaction => 1,
+ DryRun => 0,
+ @_
+ );
+
+ my ($ErrStr, @non_fatal_errors);
+
+ my $QueueObj = RT::Queue->new( RT->SystemUser );
+ if ( ref $args{'Queue'} eq 'RT::Queue' ) {
+ $QueueObj->Load( $args{'Queue'}->Id );
+ }
+ elsif ( $args{'Queue'} ) {
+ $QueueObj->Load( $args{'Queue'} );
+ }
+ else {
+ $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
+ }
+
+ #Can't create a ticket without a queue.
+ unless ( $QueueObj->Id ) {
+ $RT::Logger->debug("$self No queue given for ticket creation.");
+ return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
+ }
+
+
+ #Now that we have a queue, Check the ACLS
+ unless (
+ $self->CurrentUser->HasRight(
+ Right => 'CreateTicket',
+ Object => $QueueObj
+ )
+ )
+ {
+ return (
+ 0, 0,
+ $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
+ }
+
+ my $cycle = $QueueObj->Lifecycle;
+ unless ( defined $args{'Status'} && length $args{'Status'} ) {
+ $args{'Status'} = $cycle->DefaultOnCreate;
+ }
+
+ unless ( $cycle->IsValid( $args{'Status'} ) ) {
+ return ( 0, 0,
+ $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
+ $self->loc($args{'Status'}))
+ );
+ }
+
+ unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
+ return ( 0, 0,
+ $self->loc("New tickets can not have status '[_1]' in this queue.",
+ $self->loc($args{'Status'}))
+ );
+ }
+
+
+
+ #Since we have a queue, we can set queue defaults
+
+ #Initial Priority
+ # If there's no queue default initial priority and it's not set, set it to 0
+ $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
+ unless defined $args{'InitialPriority'};
+
+ #Final priority
+ # If there's no queue default final priority and it's not set, set it to 0
+ $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
+ unless defined $args{'FinalPriority'};
+
+ # Priority may have changed from InitialPriority, for the case
+ # where we're importing tickets (eg, from an older RT version.)
+ $args{'Priority'} = $args{'InitialPriority'}
+ unless defined $args{'Priority'};
+
+ # Dates
+ #TODO we should see what sort of due date we're getting, rather +
+ # than assuming it's in ISO format.
+
+ #Set the due date. if we didn't get fed one, use the queue default due in
+ my $Due = RT::Date->new( $self->CurrentUser );
+ if ( defined $args{'Due'} ) {
+ $Due->Set( Format => 'ISO', Value => $args{'Due'} );
+ }
+ elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
+ $Due->SetToNow;
+ $Due->AddDays( $due_in );
+ }
+
+ my $Starts = RT::Date->new( $self->CurrentUser );
+ if ( defined $args{'Starts'} ) {
+ $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
+ }
+
+ my $Started = RT::Date->new( $self->CurrentUser );
+ if ( defined $args{'Started'} ) {
+ $Started->Set( Format => 'ISO', Value => $args{'Started'} );
+ }
+
+ # If the status is not an initial status, set the started date
+ elsif ( !$cycle->IsInitial($args{'Status'}) ) {
+ $Started->SetToNow;
+ }
+
+ my $Resolved = RT::Date->new( $self->CurrentUser );
+ if ( defined $args{'Resolved'} ) {
+ $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
+ }
+
+ #If the status is an inactive status, set the resolved date
+ elsif ( $cycle->IsInactive( $args{'Status'} ) )
+ {
+ $RT::Logger->debug( "Got a ". $args{'Status'}
+ ."(inactive) ticket with undefined resolved date. Setting to now."
+ );
+ $Resolved->SetToNow;
+ }
+
+ # }}}
+
+ # Dealing with time fields
+
+ $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
+ $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
+ $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
+
+ # }}}
+
+ # Deal with setting the owner
+
+ my $Owner;
+ if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
+ if ( $args{'Owner'}->id ) {
+ $Owner = $args{'Owner'};
+ } else {
+ $RT::Logger->error('Passed an empty RT::User for owner');
+ push @non_fatal_errors,
+ $self->loc("Owner could not be set.") . " ".
+ $self->loc("Invalid value for [_1]",loc('owner'));
+ $Owner = undef;
+ }
+ }
+
+ #If we've been handed something else, try to load the user.
+ elsif ( $args{'Owner'} ) {
+ $Owner = RT::User->new( $self->CurrentUser );
+ $Owner->Load( $args{'Owner'} );
+ if (!$Owner->id) {
+ $Owner->LoadByEmail( $args{'Owner'} )
+ }
+ unless ( $Owner->Id ) {
+ push @non_fatal_errors,
+ $self->loc("Owner could not be set.") . " "
+ . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
+ $Owner = undef;
+ }
+ }
+
+ #If we have a proposed owner and they don't have the right
+ #to own a ticket, scream about it and make them not the owner
+
+ my $DeferOwner;
+ if ( $Owner && $Owner->Id != RT->Nobody->Id
+ && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
+ {
+ $DeferOwner = $Owner;
+ $Owner = undef;
+ $RT::Logger->debug('going to deffer setting owner');
+
+ }
+
+ #If we haven't been handed a valid owner, make it nobody.
+ unless ( defined($Owner) && $Owner->Id ) {
+ $Owner = RT::User->new( $self->CurrentUser );
+ $Owner->Load( RT->Nobody->Id );
+ }
+
+ # }}}
+
+# We attempt to load or create each of the people who might have a role for this ticket
+# _outside_ the transaction, so we don't get into ticket creation races
+ foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
+ $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
+ foreach my $watcher ( splice @{ $args{$type} } ) {
+ next unless $watcher;
+ if ( $watcher =~ /^\d+$/ ) {
+ push @{ $args{$type} }, $watcher;
+ } else {
+ my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
+ foreach my $address( @addresses ) {
+ my $user = RT::User->new( RT->SystemUser );
+ my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
+ unless ( $uid ) {
+ push @non_fatal_errors,
+ $self->loc("Couldn't load or create user: [_1]", $msg);
+ } else {
+ push @{ $args{$type} }, $user->id;
+ }
+ }
+ }
+ }
+ }
+
+ $RT::Handle->BeginTransaction();
+
+ my %params = (
+ Queue => $QueueObj->Id,
+ Owner => $Owner->Id,
+ Subject => $args{'Subject'},
+ InitialPriority => $args{'InitialPriority'},
+ FinalPriority => $args{'FinalPriority'},
+ Priority => $args{'Priority'},
+ Status => $args{'Status'},
+ TimeWorked => $args{'TimeWorked'},
+ TimeEstimated => $args{'TimeEstimated'},
+ TimeLeft => $args{'TimeLeft'},
+ Type => $args{'Type'},
+ Starts => $Starts->ISO,
+ Started => $Started->ISO,
+ Resolved => $Resolved->ISO,
+ Due => $Due->ISO
+ );
+
+# Parameters passed in during an import that we probably don't want to touch, otherwise
+ foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
+ $params{$attr} = $args{$attr} if $args{$attr};
+ }
+
+ # Delete null integer parameters
+ foreach my $attr
+ (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
+ {
+ delete $params{$attr}
+ unless ( exists $params{$attr} && $params{$attr} );
+ }
+
+ # Delete the time worked if we're counting it in the transaction
+ delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
+
+ my ($id,$ticket_message) = $self->SUPER::Create( %params );
+ unless ($id) {
+ $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
+ $RT::Handle->Rollback();
+ return ( 0, 0,
+ $self->loc("Ticket could not be created due to an internal error")
+ );
+ }
+
+ #Set the ticket's effective ID now that we've created it.
+ my ( $val, $msg ) = $self->__Set(
+ Field => 'EffectiveId',
+ Value => ( $args{'EffectiveId'} || $id )
+ );
+ unless ( $val ) {
+ $RT::Logger->crit("Couldn't set EffectiveId: $msg");
+ $RT::Handle->Rollback;
+ return ( 0, 0,
+ $self->loc("Ticket could not be created due to an internal error")
+ );
+ }
+
+ my $create_groups_ret = $self->_CreateTicketGroups();
+ unless ($create_groups_ret) {
+ $RT::Logger->crit( "Couldn't create ticket groups for ticket "
+ . $self->Id
+ . ". aborting Ticket creation." );
+ $RT::Handle->Rollback();
+ return ( 0, 0,
+ $self->loc("Ticket could not be created due to an internal error")
+ );
+ }
+
+ # Set the owner in the Groups table
+ # We denormalize it into the Ticket table too because doing otherwise would
+ # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
+ $self->OwnerGroup->_AddMember(
+ PrincipalId => $Owner->PrincipalId,
+ InsideTransaction => 1
+ ) unless $DeferOwner;
+
+
+
+ # Deal with setting up watchers
+
+ foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
+ # we know it's an array ref
+ foreach my $watcher ( @{ $args{$type} } ) {
+
+ # Note that we're using AddWatcher, rather than _AddWatcher, as we
+ # actually _want_ that ACL check. Otherwise, random ticket creators
+ # could make themselves adminccs and maybe get ticket rights. that would
+ # be poor
+ my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
+
+ my ($val, $msg) = $self->$method(
+ Type => $type,
+ PrincipalId => $watcher,
+ Silent => 1,
+ );
+ push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
+ unless $val;
+ }
+ }
+
+ if ($args{'SquelchMailTo'}) {
+ my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
+ : $args{'SquelchMailTo'};
+ $self->_SquelchMailTo( @squelch );
+ }
+
+
+ # }}}
+
+ # Add all the custom fields
+
+ foreach my $arg ( keys %args ) {
+ next unless $arg =~ /^CustomField-(\d+)$/i;
+ my $cfid = $1;
+
+ foreach my $value (
+ UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
+ {
+ next unless defined $value && length $value;
+
+ # Allow passing in uploaded LargeContent etc by hash reference
+ my ($status, $msg) = $self->_AddCustomFieldValue(
+ (UNIVERSAL::isa( $value => 'HASH' )
+ ? %$value
+ : (Value => $value)
+ ),
+ Field => $cfid,
+ RecordTransaction => 0,
+ );
+ push @non_fatal_errors, $msg unless $status;
+ }
+ }
+
+ # }}}
+
+ # Deal with setting up links
+
+ # TODO: Adding link may fire scrips on other end and those scrips
+ # could create transactions on this ticket before 'Create' transaction.
+ #
+ # We should implement different lifecycle: record 'Create' transaction,
+ # create links and only then fire create transaction's scrips.
+ #
+ # Ideal variant: add all links without firing scrips, record create
+ # transaction and only then fire scrips on the other ends of links.
+ #
+ # //RUZ
+
+ foreach my $type ( keys %LINKTYPEMAP ) {
+ next unless ( defined $args{$type} );
+ foreach my $link (
+ ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
+ {
+ # Check rights on the other end of the link if we must
+ # then run _AddLink that doesn't check for ACLs
+ if ( RT->Config->Get( 'StrictLinkACL' ) ) {
+ my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
+ unless ( $val ) {
+ push @non_fatal_errors, $msg;
+ next;
+ }
+ if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
+ push @non_fatal_errors, $self->loc('Linking. Permission denied');
+ next;
+ }
+ }
+
+ my ( $wval, $wmsg ) = $self->_AddLink(
+ Type => $LINKTYPEMAP{$type}->{'Type'},
+ $LINKTYPEMAP{$type}->{'Mode'} => $link,
+ Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
+ 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
+ => 1,
+ );
+
+ push @non_fatal_errors, $wmsg unless ($wval);
+ }
+ }
+
+ # }}}
+ # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
+ # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
+ if ( $DeferOwner ) {
+ if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
+
+ $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
+ . ") was proposed as a ticket owner but has no rights to own "
+ . "tickets in " . $QueueObj->Name );
+ push @non_fatal_errors, $self->loc(
+ "Owner '[_1]' does not have rights to own this ticket.",
+ $DeferOwner->Name
+ );
+ } else {
+ $Owner = $DeferOwner;
+ $self->__Set(Field => 'Owner', Value => $Owner->id);
+
+ }
+ $self->OwnerGroup->_AddMember(
+ PrincipalId => $Owner->PrincipalId,
+ InsideTransaction => 1
+ );
+ }
+
+ if ( $args{'_RecordTransaction'} ) {
+
+ # Add a transaction for the create
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => "Create",
+ TimeTaken => $args{'TimeWorked'},
+ MIMEObj => $args{'MIMEObj'},
+ CommitScrips => !$args{'DryRun'},
+ SquelchMailTo => $args{'TransSquelchMailTo'},
+ );
+
+ if ( $self->Id && $Trans ) {
+
+ $TransObj->UpdateCustomFields(ARGSRef => \%args);
+
+ $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
+ $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
+ $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+ }
+ else {
+ $RT::Handle->Rollback();
+
+ $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+ $RT::Logger->error("Ticket couldn't be created: $ErrStr");
+ return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
+ }
+
+ if ( $args{'DryRun'} ) {
+ $RT::Handle->Rollback();
+ return ($self->id, $TransObj, $ErrStr);
+ }
+ $RT::Handle->Commit();
+ return ( $self->Id, $TransObj->Id, $ErrStr );
+
+ # }}}
+ }
+ else {
+
+ # Not going to record a transaction
+ $RT::Handle->Commit();
+ $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
+ $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+ return ( $self->Id, 0, $ErrStr );
+
+ }
+}
+
+
+
+
+=head2 _Parse822HeadersForAttributes Content
+
+Takes an RFC822 style message and parses its attributes into a hash.
+
+=cut
+
+sub _Parse822HeadersForAttributes {
+ my $self = shift;
+ my $content = shift;
+ my %args;
+
+ my @lines = ( split ( /\n/, $content ) );
+ while ( defined( my $line = shift @lines ) ) {
+ if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
+ my $value = $2;
+ my $tag = lc($1);
+
+ $tag =~ s/-//g;
+ if ( defined( $args{$tag} ) )
+ { #if we're about to get a second value, make it an array
+ $args{$tag} = [ $args{$tag} ];
+ }
+ if ( ref( $args{$tag} ) )
+ { #If it's an array, we want to push the value
+ push @{ $args{$tag} }, $value;
+ }
+ else { #if there's nothing there, just set the value
+ $args{$tag} = $value;
+ }
+ } elsif ($line =~ /^$/) {
+
+ #TODO: this won't work, since "" isn't of the form "foo:value"
+
+ while ( defined( my $l = shift @lines ) ) {
+ push @{ $args{'content'} }, $l;
+ }
+ }
+
+ }
+
+ foreach my $date (qw(due starts started resolved)) {
+ my $dateobj = RT::Date->new(RT->SystemUser);
+ if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
+ $dateobj->Set( Format => 'unix', Value => $args{$date} );
+ }
+ else {
+ $dateobj->Set( Format => 'unknown', Value => $args{$date} );
+ }
+ $args{$date} = $dateobj->ISO;
+ }
+ $args{'mimeobj'} = MIME::Entity->new();
+ $args{'mimeobj'}->build(
+ Type => ( $args{'contenttype'} || 'text/plain' ),
+ Data => ($args{'content'} || '')
+ );
+
+ return (%args);
+}
+
+
+
+=head2 Import PARAMHASH
+
+Import a ticket.
+Doesn\'t create a transaction.
+Doesn\'t supply queue defaults, etc.
+
+Returns: TICKETID
+
+=cut
+
+sub Import {
+ my $self = shift;
+ my ( $ErrStr, $QueueObj, $Owner );
+
+ my %args = (
+ id => undef,
+ EffectiveId => undef,
+ Queue => undef,
+ Requestor => undef,
+ Type => 'ticket',
+ Owner => RT->Nobody->Id,
+ Subject => '[no subject]',
+ InitialPriority => undef,
+ FinalPriority => undef,
+ Status => 'new',
+ TimeWorked => "0",
+ Due => undef,
+ Created => undef,
+ Updated => undef,
+ Resolved => undef,
+ Told => undef,
+ @_
+ );
+
+ if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
+ $QueueObj = RT::Queue->new(RT->SystemUser);
+ $QueueObj->Load( $args{'Queue'} );
+
+ #TODO error check this and return 0 if it\'s not loading properly +++
+ }
+ elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
+ $QueueObj = RT::Queue->new(RT->SystemUser);
+ $QueueObj->Load( $args{'Queue'}->Id );
+ }
+ else {
+ $RT::Logger->debug(
+ "$self " . $args{'Queue'} . " not a recognised queue object." );
+ }
+
+ #Can't create a ticket without a queue.
+ unless ( defined($QueueObj) and $QueueObj->Id ) {
+ $RT::Logger->debug("$self No queue given for ticket creation.");
+ return ( 0, $self->loc('Could not create ticket. Queue not set') );
+ }
+
+ #Now that we have a queue, Check the ACLS
+ unless (
+ $self->CurrentUser->HasRight(
+ Right => 'CreateTicket',
+ Object => $QueueObj
+ )
+ )
+ {
+ return ( 0,
+ $self->loc("No permission to create tickets in the queue '[_1]'"
+ , $QueueObj->Name));
+ }
+
+ # Deal with setting the owner
+
+ # Attempt to take user object, user name or user id.
+ # Assign to nobody if lookup fails.
+ if ( defined( $args{'Owner'} ) ) {
+ if ( ref( $args{'Owner'} ) ) {
+ $Owner = $args{'Owner'};
+ }
+ else {
+ $Owner = RT::User->new( $self->CurrentUser );
+ $Owner->Load( $args{'Owner'} );
+ if ( !defined( $Owner->id ) ) {
+ $Owner->Load( RT->Nobody->id );
+ }
+ }
+ }
+
+ #If we have a proposed owner and they don't have the right
+ #to own a ticket, scream about it and make them not the owner
+ if (
+ ( defined($Owner) )
+ and ( $Owner->Id != RT->Nobody->Id )
+ and (
+ !$Owner->HasRight(
+ Object => $QueueObj,
+ Right => 'OwnTicket'
+ )
+ )
+ )
+ {
+
+ $RT::Logger->warning( "$self user "
+ . $Owner->Name . "("
+ . $Owner->id
+ . ") was proposed "
+ . "as a ticket owner but has no rights to own "
+ . "tickets in '"
+ . $QueueObj->Name . "'" );
+
+ $Owner = undef;
+ }
+
+ #If we haven't been handed a valid owner, make it nobody.
+ unless ( defined($Owner) ) {
+ $Owner = RT::User->new( $self->CurrentUser );
+ $Owner->Load( RT->Nobody->UserObj->Id );
+ }
+
+ # }}}
+
+ unless ( $self->ValidateStatus( $args{'Status'} ) ) {
+ return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
+ }
+
+ $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
+ $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
+ $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
+ $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
+
+ # If we're coming in with an id, set that now.
+ my $EffectiveId = undef;
+ if ( $args{'id'} ) {
+ $EffectiveId = $args{'id'};
+
+ }
+
+ my $id = $self->SUPER::Create(
+ id => $args{'id'},
+ EffectiveId => $EffectiveId,
+ Queue => $QueueObj->Id,
+ Owner => $Owner->Id,
+ Subject => $args{'Subject'}, # loc
+ InitialPriority => $args{'InitialPriority'}, # loc
+ FinalPriority => $args{'FinalPriority'}, # loc
+ Priority => $args{'InitialPriority'}, # loc
+ Status => $args{'Status'}, # loc
+ TimeWorked => $args{'TimeWorked'}, # loc
+ Type => $args{'Type'}, # loc
+ Created => $args{'Created'}, # loc
+ Told => $args{'Told'}, # loc
+ LastUpdated => $args{'Updated'}, # loc
+ Resolved => $args{'Resolved'}, # loc
+ Due => $args{'Due'}, # loc
+ );
+
+ # If the ticket didn't have an id
+ # Set the ticket's effective ID now that we've created it.
+ if ( $args{'id'} ) {
+ $self->Load( $args{'id'} );
+ }
+ else {
+ my ( $val, $msg ) =
+ $self->__Set( Field => 'EffectiveId', Value => $id );
+
+ unless ($val) {
+ $RT::Logger->err(
+ $self . "->Import couldn't set EffectiveId: $msg" );
+ }
+ }
+
+ my $create_groups_ret = $self->_CreateTicketGroups();
+ unless ($create_groups_ret) {
+ $RT::Logger->crit(
+ "Couldn't create ticket groups for ticket " . $self->Id );
+ }
+
+ $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
+
- my $watcher;
- foreach $watcher ( @{ $args{'Cc'} } ) {
++ foreach my $watcher ( @{ $args{'Cc'} } ) {
+ $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
+ }
- foreach $watcher ( @{ $args{'AdminCc'} } ) {
++ foreach my $watcher ( @{ $args{'AdminCc'} } ) {
+ $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
+ Silent => 1 );
+ }
- foreach $watcher ( @{ $args{'Requestor'} } ) {
++ foreach my $watcher ( @{ $args{'Requestor'} } ) {
+ $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
+ Silent => 1 );
+ }
+
+ return ( $self->Id, $ErrStr );
+}
+
+
+
+
+=head2 _CreateTicketGroups
+
+Create the ticket groups and links for this ticket.
+This routine expects to be called from Ticket->Create _inside of a transaction_
+
+It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
+
+It will return true on success and undef on failure.
+
+
+=cut
+
+
+sub _CreateTicketGroups {
+ my $self = shift;
+
+ my @types = (qw(Requestor Owner Cc AdminCc));
+
+ foreach my $type (@types) {
+ my $type_obj = RT::Group->new($self->CurrentUser);
+ my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
+ Instance => $self->Id,
+ Type => $type);
+ unless ($id) {
+ $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
+ $self->Id.": ".$msg);
+ return(undef);
+ }
+ }
+ return(1);
+
+}
+
+
+
+=head2 OwnerGroup
+
+A constructor which returns an RT::Group object containing the owner of this ticket.
+
+=cut
+
+sub OwnerGroup {
+ my $self = shift;
+ my $owner_obj = RT::Group->new($self->CurrentUser);
+ $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
+ return ($owner_obj);
+}
+
+
+
+
+=head2 AddWatcher
+
+AddWatcher takes a parameter hash. The keys are as follows:
+
+Type One of Requestor, Cc, AdminCc
+
+PrincipalId The RT::Principal id of the user or group that's being added as a watcher
+
+Email The email address of the new watcher. If a user with this
+ email address can't be found, a new nonprivileged user will be created.
+
+If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
+
+=cut
+
+sub AddWatcher {
+ my $self = shift;
+ my %args = (
+ Type => undef,
+ PrincipalId => undef,
+ Email => undef,
+ @_
+ );
+
+ # ModifyTicket works in any case
+ return $self->_AddWatcher( %args )
+ if $self->CurrentUserHasRight('ModifyTicket');
+ if ( $args{'Email'} ) {
+ my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
+ return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
+ unless $addr;
+
+ if ( lc $self->CurrentUser->UserObj->EmailAddress
+ eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
+ {
+ $args{'PrincipalId'} = $self->CurrentUser->id;
+ delete $args{'Email'};
+ }
+ }
+
+ # If the watcher isn't the current user then the current user has no right
+ # bail
+ unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
+ if ( $args{'Type'} eq 'AdminCc' ) {
+ unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+
+ # If it's a Requestor or Cc and they don't have 'Watch', bail
+ elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
+ unless ( $self->CurrentUserHasRight('Watch') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ else {
+ $RT::Logger->warning( "AddWatcher got passed a bogus type");
+ return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
+ }
+
+ return $self->_AddWatcher( %args );
+}
+
+#This contains the meat of AddWatcher. but can be called from a routine like
+# Create, which doesn't need the additional acl check
+sub _AddWatcher {
+ my $self = shift;
+ my %args = (
+ Type => undef,
+ Silent => undef,
+ PrincipalId => undef,
+ Email => undef,
+ @_
+ );
+
+
+ my $principal = RT::Principal->new($self->CurrentUser);
+ if ($args{'Email'}) {
+ if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
+ return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
+ }
+ my $user = RT::User->new(RT->SystemUser);
+ my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
+ $args{'PrincipalId'} = $pid if $pid;
+ }
+ if ($args{'PrincipalId'}) {
+ $principal->Load($args{'PrincipalId'});
+ if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
+ return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
+ if RT::EmailParser->IsRTAddress( $email );
+
+ }
+ }
+
+
+ # If we can't find this watcher, we need to bail.
+ unless ($principal->Id) {
+ $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
+ return(0, $self->loc("Could not find or create that user"));
+ }
+
+
+ my $group = RT::Group->new($self->CurrentUser);
+ $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
+ unless ($group->id) {
+ return(0,$self->loc("Group not found"));
+ }
+
+ if ( $group->HasMember( $principal)) {
+
+ return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
+ }
+
+
+ my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
+ InsideTransaction => 1 );
+ unless ($m_id) {
+ $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
+
+ return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
+ }
+
+ unless ( $args{'Silent'} ) {
+ $self->_NewTransaction(
+ Type => 'AddWatcher',
+ NewValue => $principal->Id,
+ Field => $args{'Type'}
+ );
+ }
+
+ return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
+}
+
+
+
+
+=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
+
+
+Deletes a Ticket watcher. Takes two arguments:
+
+Type (one of Requestor,Cc,AdminCc)
+
+and one of
+
+PrincipalId (an RT::Principal Id of the watcher you want to remove)
+ OR
+Email (the email address of an existing wathcer)
+
+
+=cut
+
+
+sub DeleteWatcher {
+ my $self = shift;
+
+ my %args = ( Type => undef,
+ PrincipalId => undef,
+ Email => undef,
+ @_ );
+
+ unless ( $args{'PrincipalId'} || $args{'Email'} ) {
+ return ( 0, $self->loc("No principal specified") );
+ }
+ my $principal = RT::Principal->new( $self->CurrentUser );
+ if ( $args{'PrincipalId'} ) {
+
+ $principal->Load( $args{'PrincipalId'} );
+ }
+ else {
+ my $user = RT::User->new( $self->CurrentUser );
+ $user->LoadByEmail( $args{'Email'} );
+ $principal->Load( $user->Id );
+ }
+
+ # If we can't find this watcher, we need to bail.
+ unless ( $principal->Id ) {
+ return ( 0, $self->loc("Could not find that principal") );
+ }
+
+ my $group = RT::Group->new( $self->CurrentUser );
+ $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
+ unless ( $group->id ) {
+ return ( 0, $self->loc("Group not found") );
+ }
+
+ # Check ACLS
+ #If the watcher we're trying to add is for the current user
+ if ( $self->CurrentUser->PrincipalId == $principal->id ) {
+
+ # If it's an AdminCc and they don't have
+ # 'WatchAsAdminCc' or 'ModifyTicket', bail
+ if ( $args{'Type'} eq 'AdminCc' ) {
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+
+ # If it's a Requestor or Cc and they don't have
+ # 'Watch' or 'ModifyTicket', bail
+ elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
+ {
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ or $self->CurrentUserHasRight('Watch') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ else {
+ $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
+ return ( 0,
+ $self->loc('Error in parameters to Ticket->DeleteWatcher') );
+ }
+ }
+
+ # If the watcher isn't the current user
+ # and the current user doesn't have 'ModifyTicket' bail
+ else {
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ # }}}
+
+ # see if this user is already a watcher.
+
+ unless ( $group->HasMember($principal) ) {
+ return ( 0,
+ $self->loc( 'That principal is not a [_1] for this ticket',
+ $args{'Type'} ) );
+ }
+
+ my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
+ unless ($m_id) {
+ $RT::Logger->error( "Failed to delete "
+ . $principal->Id
+ . " as a member of group "
+ . $group->Id . ": "
+ . $m_msg );
+
+ return (0,
+ $self->loc(
+ 'Could not remove that principal as a [_1] for this ticket',
+ $args{'Type'} ) );
+ }
+
+ unless ( $args{'Silent'} ) {
+ $self->_NewTransaction( Type => 'DelWatcher',
+ OldValue => $principal->Id,
+ Field => $args{'Type'} );
+ }
+
+ return ( 1,
+ $self->loc( "[_1] is no longer a [_2] for this ticket.",
+ $principal->Object->Name,
+ $args{'Type'} ) );
+}
+
+
+
+
+
+=head2 SquelchMailTo [EMAIL]
+
+Takes an optional email address to never email about updates to this ticket.
+
+
+Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
+
+
+=cut
+
+sub SquelchMailTo {
+ my $self = shift;
+ if (@_) {
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ();
+ }
+ } else {
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ return ();
+ }
+
+ }
+ return $self->_SquelchMailTo(@_);
+}
+
+sub _SquelchMailTo {
+ my $self = shift;
+ if (@_) {
+ my $attr = shift;
+ $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
+ unless grep { $_->Content eq $attr }
+ $self->Attributes->Named('SquelchMailTo');
+ }
+ my @attributes = $self->Attributes->Named('SquelchMailTo');
+ return (@attributes);
+}
+
+
+=head2 UnsquelchMailTo ADDRESS
+
+Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
+
+Returns a tuple of (status, message)
+
+=cut
+
+sub UnsquelchMailTo {
+ my $self = shift;
+
+ my $address = shift;
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
+ return ($val, $msg);
+}
+
+
+
+=head2 RequestorAddresses
+
+ B<Returns> String: All Ticket Requestor email addresses as a string.
+
+=cut
+
+sub RequestorAddresses {
+ my $self = shift;
+
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ return undef;
+ }
+
+ return ( $self->Requestors->MemberEmailAddressesAsString );
+}
+
+
+=head2 AdminCcAddresses
+
+returns String: All Ticket AdminCc email addresses as a string
+
+=cut
+
+sub AdminCcAddresses {
+ my $self = shift;
+
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ return undef;
+ }
+
+ return ( $self->AdminCc->MemberEmailAddressesAsString )
+
+}
+
+=head2 CcAddresses
+
+returns String: All Ticket Ccs as a string of email addresses
+
+=cut
+
+sub CcAddresses {
+ my $self = shift;
+
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ return undef;
+ }
+ return ( $self->Cc->MemberEmailAddressesAsString);
+
+}
+
+
+
+
+=head2 Requestors
+
+Takes nothing.
+Returns this ticket's Requestors as an RT::Group object
+
+=cut
+
+sub Requestors {
+ my $self = shift;
+
+ my $group = RT::Group->new($self->CurrentUser);
+ if ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
+ }
+ return ($group);
+
+}
+
+
+
+=head2 Cc
+
+Takes nothing.
+Returns an RT::Group object which contains this ticket's Ccs.
+If the user doesn't have "ShowTicket" permission, returns an empty group
+
+=cut
+
+sub Cc {
+ my $self = shift;
+
+ my $group = RT::Group->new($self->CurrentUser);
+ if ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
+ }
+ return ($group);
+
+}
+
+
+
+=head2 AdminCc
+
+Takes nothing.
+Returns an RT::Group object which contains this ticket's AdminCcs.
+If the user doesn't have "ShowTicket" permission, returns an empty group
+
+=cut
+
+sub AdminCc {
+ my $self = shift;
+
+ my $group = RT::Group->new($self->CurrentUser);
+ if ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
+ }
+ return ($group);
+
+}
+
+
+
+
+# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
+
+=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
+
+Takes a param hash with the attributes Type and either PrincipalId or Email
+
+Type is one of Requestor, Cc, AdminCc and Owner
+
+PrincipalId is an RT::Principal id, and Email is an email address.
+
+Returns true if the specified principal (or the one corresponding to the
+specified address) is a member of the group Type for this ticket.
+
+XX TODO: This should be Memoized.
+
+=cut
+
+sub IsWatcher {
+ my $self = shift;
+
+ my %args = ( Type => 'Requestor',
+ PrincipalId => undef,
+ Email => undef,
+ @_
+ );
+
+ # Load the relevant group.
+ my $group = RT::Group->new($self->CurrentUser);
+ $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
+
+ # Find the relevant principal.
+ if (!$args{PrincipalId} && $args{Email}) {
+ # Look up the specified user.
+ my $user = RT::User->new($self->CurrentUser);
+ $user->LoadByEmail($args{Email});
+ if ($user->Id) {
+ $args{PrincipalId} = $user->PrincipalId;
+ }
+ else {
+ # A non-existent user can't be a group member.
+ return 0;
+ }
+ }
+
+ # Ask if it has the member in question
+ return $group->HasMember( $args{'PrincipalId'} );
+}
+
+
+
+=head2 IsRequestor PRINCIPAL_ID
+
+Takes an L<RT::Principal> id.
+
+Returns true if the principal is a requestor of the current ticket.
+
+=cut
+
+sub IsRequestor {
+ my $self = shift;
+ my $person = shift;
+
+ return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
+
+};
+
+
+
+=head2 IsCc PRINCIPAL_ID
+
+ Takes an RT::Principal id.
+ Returns true if the principal is a Cc of the current ticket.
+
+
+=cut
+
+sub IsCc {
+ my $self = shift;
+ my $cc = shift;
+
+ return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
+
+}
+
+
+
+=head2 IsAdminCc PRINCIPAL_ID
+
+ Takes an RT::Principal id.
+ Returns true if the principal is an AdminCc of the current ticket.
+
+=cut
+
+sub IsAdminCc {
+ my $self = shift;
+ my $person = shift;
+
+ return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
+
+}
+
+
+
+=head2 IsOwner
+
+ Takes an RT::User object. Returns true if that user is this ticket's owner.
+returns undef otherwise
+
+=cut
+
+sub IsOwner {
+ my $self = shift;
+ my $person = shift;
+
+ # no ACL check since this is used in acl decisions
+ # unless ($self->CurrentUserHasRight('ShowTicket')) {
+ # return(undef);
+ # }
+
+ #Tickets won't yet have owners when they're being created.
+ unless ( $self->OwnerObj->id ) {
+ return (undef);
+ }
+
+ if ( $person->id == $self->OwnerObj->id ) {
+ return (1);
+ }
+ else {
+ return (undef);
+ }
+}
+
+
+
+
+
+=head2 TransactionAddresses
+
+Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
+all this ticket's Create, Comment or Correspond transactions. The keys are
+stringified email addresses. Each value is an L<Email::Address> object.
+
+NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
+
+=cut
+
+
+sub TransactionAddresses {
+ my $self = shift;
+ my $txns = $self->Transactions;
+
+ my %addresses = ();
+
+ my $attachments = RT::Attachments->new( $self->CurrentUser );
+ $attachments->LimitByTicket( $self->id );
+ $attachments->Columns( qw( id Headers TransactionId));
+
+
+ foreach my $type (qw(Create Comment Correspond)) {
+ $attachments->Limit( ALIAS => $attachments->TransactionAlias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $type,
+ ENTRYAGGREGATOR => 'OR',
+ CASESENSITIVE => 1
+ );
+ }
+
+ while ( my $att = $attachments->Next ) {
+ foreach my $addrlist ( values %{$att->Addresses } ) {
+ foreach my $addr (@$addrlist) {
+
+# Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
+ next
+ if ( $addresses{ $addr->address }
+ && $addresses{ $addr->address }->phrase
+ && not $addr->phrase );
+
+ # skips "comment-only" addresses
+ next unless ( $addr->address );
+ $addresses{ $addr->address } = $addr;
+ }
+ }
+ }
+
+ return \%addresses;
+
+}
+
+
+
+
+
+
+sub ValidateQueue {
+ my $self = shift;
+ my $Value = shift;
+
+ if ( !$Value ) {
+ $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
+ return (1);
+ }
+
+ my $QueueObj = RT::Queue->new( $self->CurrentUser );
+ my $id = $QueueObj->Load($Value);
+
+ if ($id) {
+ return (1);
+ }
+ else {
+ return (undef);
+ }
+}
+
+
+
+sub SetQueue {
+ my $self = shift;
+ my $NewQueue = shift;
+
+ #Redundant. ACL gets checked in _Set;
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
+ $NewQueueObj->Load($NewQueue);
+
+ unless ( $NewQueueObj->Id() ) {
+ return ( 0, $self->loc("That queue does not exist") );
+ }
+
+ if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
+ return ( 0, $self->loc('That is the same value') );
+ }
+ unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
+ return ( 0, $self->loc("You may not create requests in that queue.") );
+ }
+
+ my $new_status;
+ my $old_lifecycle = $self->QueueObj->Lifecycle;
+ my $new_lifecycle = $NewQueueObj->Lifecycle;
+ if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
+ unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
+ return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
+ }
+ $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
+ return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
+ unless $new_status;
+ }
+
+ if ( $new_status ) {
+ my $clone = RT::Ticket->new( RT->SystemUser );
+ $clone->Load( $self->Id );
+ unless ( $clone->Id ) {
+ return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
+ }
+
+ my $now = RT::Date->new( $self->CurrentUser );
+ $now->SetToNow;
+
+ my $old_status = $clone->Status;
+
+ #If we're changing the status from initial in old to not intial in new,
+ # record that we've started
+ if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
+ #Set the Started time to "now"
+ $clone->_Set(
+ Field => 'Started',
+ Value => $now->ISO,
+ RecordTransaction => 0
+ );
+ }
+
+ #When we close a ticket, set the 'Resolved' attribute to now.
+ # It's misnamed, but that's just historical.
+ if ( $new_lifecycle->IsInactive($new_status) ) {
+ $clone->_Set(
+ Field => 'Resolved',
+ Value => $now->ISO,
+ RecordTransaction => 0,
+ );
+ }
+
+ #Actually update the status
+ my ($val, $msg)= $clone->_Set(
+ Field => 'Status',
+ Value => $new_status,
+ RecordTransaction => 0,
+ );
+ $RT::Logger->error( 'Status change failed on queue change: '. $msg )
+ unless $val;
+ }
+
+ my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
+
+ if ( $status ) {
+ # Clear the queue object cache;
+ $self->{_queue_obj} = undef;
+
+ # Untake the ticket if we have no permissions in the new queue
+ unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
+ my $clone = RT::Ticket->new( RT->SystemUser );
+ $clone->Load( $self->Id );
+ unless ( $clone->Id ) {
+ return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
+ }
+ my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
+ $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
+ }
+
+ # On queue change, change queue for reminders too
+ my $reminder_collection = $self->Reminders->Collection;
+ while ( my $reminder = $reminder_collection->Next ) {
+ my ($status, $msg) = $reminder->SetQueue($NewQueue);
+ $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
+ }
+ }
+
+ return ($status, $msg);
+}
+
+
+
+=head2 QueueObj
+
+Takes nothing. returns this ticket's queue object
+
+=cut
+
+sub QueueObj {
+ my $self = shift;
+
+ if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
+
+ $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
+
+ #We call __Value so that we can avoid the ACL decision and some deep recursion
+ my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
+ }
+ return ($self->{_queue_obj});
+}
+
+=head2 SubjectTag
+
+Takes nothing. Returns SubjectTag for this ticket. Includes
+queue's subject tag or rtname if that is not set, ticket
+id and braces, for example:
+
+ [support.example.com #123456]
+
+=cut
+
+sub SubjectTag {
+ my $self = shift;
+ return
+ '['
+ . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
+ .' #'. $self->id
+ .']'
+ ;
+}
+
+
+=head2 DueObj
+
+ Returns an RT::Date object containing this ticket's due date
+
+=cut
+
+sub DueObj {
+ my $self = shift;
+
+ my $time = RT::Date->new( $self->CurrentUser );
+
+ # -1 is RT::Date slang for never
+ if ( my $due = $self->Due ) {
+ $time->Set( Format => 'sql', Value => $due );
+ }
+ else {
+ $time->Set( Format => 'unix', Value => -1 );
+ }
+
+ return $time;
+}
+
+
+
+=head2 DueAsString
+
+Returns this ticket's due date as a human readable string
+
+=cut
+
+sub DueAsString {
+ my $self = shift;
+ return $self->DueObj->AsString();
+}
+
+
+
+=head2 ResolvedObj
+
+ Returns an RT::Date object of this ticket's 'resolved' time.
+
+=cut
+
+sub ResolvedObj {
+ my $self = shift;
+
+ my $time = RT::Date->new( $self->CurrentUser );
+ $time->Set( Format => 'sql', Value => $self->Resolved );
+ return $time;
+}
+
+
+=head2 FirstActiveStatus
+
+Returns the first active status that the ticket could transition to,
+according to its current Queue's lifecycle. May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in L<RT::Action::AutoOpen>, for instance.
+
+=cut
+
+sub FirstActiveStatus {
+ my $self = shift;
+
+ my $lifecycle = $self->QueueObj->Lifecycle;
+ my $status = $self->Status;
+ my @active = $lifecycle->Active;
+ # no change if no active statuses in the lifecycle
+ return undef unless @active;
+
+ # no change if the ticket is already has first status from the list of active
+ return undef if lc $status eq lc $active[0];
+
+ my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
+ return $next;
+}
+
+=head2 SetStarted
+
+Takes a date in ISO format or undef
+Returns a transaction id and a message
+The client calls "Start" to note that the project was started on the date in $date.
+A null date means "now"
+
+=cut
+
+sub SetStarted {
+ my $self = shift;
+ my $time = shift || 0;
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ #We create a date object to catch date weirdness
+ my $time_obj = RT::Date->new( $self->CurrentUser() );
+ if ( $time ) {
+ $time_obj->Set( Format => 'ISO', Value => $time );
+ }
+ else {
+ $time_obj->SetToNow();
+ }
+
+ # We need $TicketAsSystem, in case the current user doesn't have
+ # ShowTicket
+ my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
+ $TicketAsSystem->Load( $self->Id );
+ # Now that we're starting, open this ticket
+ # TODO: do we really want to force this as policy? it should be a scrip
+ my $next = $TicketAsSystem->FirstActiveStatus;
+
+ $self->SetStatus( $next ) if defined $next;
+
+ return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
+
+}
+
+
+
+=head2 StartedObj
+
+ Returns an RT::Date object which contains this ticket's
+'Started' time.
+
+=cut
+
+sub StartedObj {
+ my $self = shift;
+
+ my $time = RT::Date->new( $self->CurrentUser );
+ $time->Set( Format => 'sql', Value => $self->Started );
+ return $time;
+}
+
+
+
+=head2 StartsObj
+
+ Returns an RT::Date object which contains this ticket's
+'Starts' time.
+
+=cut
+
+sub StartsObj {
+ my $self = shift;
+
+ my $time = RT::Date->new( $self->CurrentUser );
+ $time->Set( Format => 'sql', Value => $self->Starts );
+ return $time;
+}
+
+
+
+=head2 ToldObj
+
+ Returns an RT::Date object which contains this ticket's
+'Told' time.
+
+=cut
+
+sub ToldObj {
+ my $self = shift;
+
+ my $time = RT::Date->new( $self->CurrentUser );
+ $time->Set( Format => 'sql', Value => $self->Told );
+ return $time;
+}
+
+
+
+=head2 ToldAsString
+
+A convenience method that returns ToldObj->AsString
+
+TODO: This should be deprecated
+
+=cut
+
+sub ToldAsString {
+ my $self = shift;
+ if ( $self->Told ) {
+ return $self->ToldObj->AsString();
+ }
+ else {
+ return ("Never");
+ }
+}
+
+
+
+=head2 TimeWorkedAsString
+
+Returns the amount of time worked on this ticket as a Text String
+
+=cut
+
+sub TimeWorkedAsString {
+ my $self = shift;
+ my $value = $self->TimeWorked;
+
+ # return the # of minutes worked turned into seconds and written as
+ # a simple text string, this is not really a date object, but if we
+ # diff a number of seconds vs the epoch, we'll get a nice description
+ # of time worked.
+ return "" unless $value;
+ return RT::Date->new( $self->CurrentUser )
+ ->DurationAsString( $value * 60 );
+}
+
+
+
+=head2 TimeLeftAsString
+
+Returns the amount of time left on this ticket as a Text String
+
+=cut
+
+sub TimeLeftAsString {
+ my $self = shift;
+ my $value = $self->TimeLeft;
+ return "" unless $value;
+ return RT::Date->new( $self->CurrentUser )
+ ->DurationAsString( $value * 60 );
+}
+
+
+
+
+=head2 Comment
+
+Comment on this ticket.
+Takes a hash with the following attributes:
+If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
+comment.
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
+
+If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
+They will, however, be prepared and you'll be able to access them through the TransactionObj
+
+Returns: Transaction id, Error Message, Transaction Object
+(note the different order from Create()!)
+
+=cut
+
+sub Comment {
+ my $self = shift;
+
+ my %args = ( CcMessageTo => undef,
+ BccMessageTo => undef,
+ MIMEObj => undef,
+ Content => undef,
+ TimeTaken => 0,
+ DryRun => 0,
+ @_ );
+
+ unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
+ or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
+ return ( 0, $self->loc("Permission Denied"), undef );
+ }
+ $args{'NoteType'} = 'Comment';
+
+ if ($args{'DryRun'}) {
+ $RT::Handle->BeginTransaction();
+ $args{'CommitScrips'} = 0;
+ }
+
+ my @results = $self->_RecordNote(%args);
+ if ($args{'DryRun'}) {
+ $RT::Handle->Rollback();
+ }
+
+ return(@results);
+}
+
+
+=head2 Correspond
+
+Correspond on this ticket.
+Takes a hashref with the following attributes:
+
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
+
+if there's no MIMEObj, Content is used to build a MIME::Entity object
+
+If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
+They will, however, be prepared and you'll be able to access them through the TransactionObj
+
+Returns: Transaction id, Error Message, Transaction Object
+(note the different order from Create()!)
+
+
+=cut
+
+sub Correspond {
+ my $self = shift;
+ my %args = ( CcMessageTo => undef,
+ BccMessageTo => undef,
+ MIMEObj => undef,
+ Content => undef,
+ TimeTaken => 0,
+ @_ );
+
+ unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
+ or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
+ return ( 0, $self->loc("Permission Denied"), undef );
+ }
+
+ $args{'NoteType'} = 'Correspond';
+ if ($args{'DryRun'}) {
+ $RT::Handle->BeginTransaction();
+ $args{'CommitScrips'} = 0;
+ }
+
+ my @results = $self->_RecordNote(%args);
+
+ #Set the last told date to now if this isn't mail from the requestor.
+ #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
+ unless ( $self->IsRequestor($self->CurrentUser->id) ) {
+ my %squelch;
+ $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
+ $self->_SetTold
+ if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
+ }
+
+ if ($args{'DryRun'}) {
+ $RT::Handle->Rollback();
+ }
+
+ return (@results);
+
+}
+
+
+
+=head2 _RecordNote
+
+the meat of both comment and correspond.
+
+Performs no access control checks. hence, dangerous.
+
+=cut
+
+sub _RecordNote {
+ my $self = shift;
+ my %args = (
+ CcMessageTo => undef,
+ BccMessageTo => undef,
+ Encrypt => undef,
+ Sign => undef,
+ MIMEObj => undef,
+ Content => undef,
+ NoteType => 'Correspond',
+ TimeTaken => 0,
+ CommitScrips => 1,
+ SquelchMailTo => undef,
+ @_
+ );
+
+ unless ( $args{'MIMEObj'} || $args{'Content'} ) {
+ return ( 0, $self->loc("No message attached"), undef );
+ }
+
+ unless ( $args{'MIMEObj'} ) {
+ $args{'MIMEObj'} = MIME::Entity->build(
+ Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
+ );
+ }
+
+ # convert text parts into utf-8
+ RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
+
+ # If we've been passed in CcMessageTo and BccMessageTo fields,
+ # add them to the mime object for passing on to the transaction handler
+ # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
+ # RT-Send-Bcc: headers
+
+
+ foreach my $type (qw/Cc Bcc/) {
+ if ( defined $args{ $type . 'MessageTo' } ) {
+
+ my $addresses = join ', ', (
+ map { RT::User->CanonicalizeEmailAddress( $_->address ) }
+ Email::Address->parse( $args{ $type . 'MessageTo' } ) );
+ $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
+ }
+ }
+
+ foreach my $argument (qw(Encrypt Sign)) {
+ $args{'MIMEObj'}->head->add(
+ "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
+ ) if defined $args{ $argument };
+ }
+
+ # If this is from an external source, we need to come up with its
+ # internal Message-ID now, so all emails sent because of this
+ # message have a common Message-ID
+ my $org = RT->Config->Get('Organization');
+ my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
+ unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
+ $args{'MIMEObj'}->head->set(
+ 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
+ );
+ }
+
+ #Record the correspondence (write the transaction)
+ my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
+ Type => $args{'NoteType'},
+ Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
+ TimeTaken => $args{'TimeTaken'},
+ MIMEObj => $args{'MIMEObj'},
+ CommitScrips => $args{'CommitScrips'},
+ SquelchMailTo => $args{'SquelchMailTo'},
+ );
+
+ unless ($Trans) {
+ $RT::Logger->err("$self couldn't init a transaction $msg");
+ return ( $Trans, $self->loc("Message could not be recorded"), undef );
+ }
+
+ return ( $Trans, $self->loc("Message recorded"), $TransObj );
+}
+
+
+=head2 DryRun
+
+Builds a MIME object from the given C<UpdateSubject> and
+C<UpdateContent>, then calls L</Comment> or L</Correspond> with
+C<< DryRun => 1 >>, and returns the transaction so produced.
+
+=cut
+
+sub DryRun {
+ my $self = shift;
+ my %args = @_;
+ my $action;
+ if ($args{'UpdateType'} || $args{Action} =~ /^respon(d|se)$/i ) {
+ $action = 'Correspond';
+ } else {
+ $action = 'Comment';
+ }
+
+ my $Message = MIME::Entity->build(
+ Type => 'text/plain',
+ Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
+ Charset => 'UTF-8',
+ Data => $args{'UpdateContent'} || "",
+ );
+
+ my ( $Transaction, $Description, $Object ) = $self->$action(
+ CcMessageTo => $args{'UpdateCc'},
+ BccMessageTo => $args{'UpdateBcc'},
+ MIMEObj => $Message,
+ TimeTaken => $args{'UpdateTimeWorked'},
+ DryRun => 1,
+ );
+ unless ( $Transaction ) {
+ $RT::Logger->error("Couldn't fire '$action' action: $Description");
+ }
+
+ return $Object;
+}
+
+=head2 DryRunCreate
+
+Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
+C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
+the resulting L<RT::Transaction>.
+
+=cut
+
+sub DryRunCreate {
+ my $self = shift;
+ my %args = @_;
+ my $Message = MIME::Entity->build(
+ Type => 'text/plain',
+ Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
+ (defined $args{'Cc'} ?
+ ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
+ Charset => 'UTF-8',
+ Data => $args{'Content'} || "",
+ );
+
+ my ( $Transaction, $Object, $Description ) = $self->Create(
+ Type => $args{'Type'} || 'ticket',
+ Queue => $args{'Queue'},
+ Owner => $args{'Owner'},
+ Requestor => $args{'Requestors'},
+ Cc => $args{'Cc'},
+ AdminCc => $args{'AdminCc'},
+ InitialPriority => $args{'InitialPriority'},
+ FinalPriority => $args{'FinalPriority'},
+ TimeLeft => $args{'TimeLeft'},
+ TimeEstimated => $args{'TimeEstimated'},
+ TimeWorked => $args{'TimeWorked'},
+ Subject => $args{'Subject'},
+ Status => $args{'Status'},
+ MIMEObj => $Message,
+ DryRun => 1,
+ );
+ unless ( $Transaction ) {
+ $RT::Logger->error("Couldn't fire Create action: $Description");
+ }
+
+ return $Object;
+}
+
+
+
+sub _Links {
+ my $self = shift;
+
+ #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
+ #tobias meant by $f
+ my $field = shift;
+ my $type = shift || "";
+
+ my $cache_key = "$field$type";
+ return $self->{ $cache_key } if $self->{ $cache_key };
+
+ my $links = $self->{ $cache_key }
+ = RT::Links->new( $self->CurrentUser );
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $links->Limit( FIELD => 'id', VALUE => 0 );
+ return $links;
+ }
+
+ # Maybe this ticket is a merge ticket
+ my $limit_on = 'Local'. $field;
+ # at least to myself
+ $links->Limit(
+ FIELD => $limit_on,
+ VALUE => $self->id,
+ ENTRYAGGREGATOR => 'OR',
+ );
+ $links->Limit(
+ FIELD => $limit_on,
+ VALUE => $_,
+ ENTRYAGGREGATOR => 'OR',
+ ) foreach $self->Merged;
+ $links->Limit(
+ FIELD => 'Type',
+ VALUE => $type,
+ ) if $type;
+
+ return $links;
+}
+
+
+
+=head2 DeleteLink
+
+Delete a link. takes a paramhash of Base, Target, Type, Silent,
+SilentBase and SilentTarget. Either Base or Target must be null.
+The null value will be replaced with this ticket\'s id.
+
+If Silent is true then no transaction would be recorded, in other
+case you can control creation of transactions on both base and
+target with SilentBase and SilentTarget respectively. By default
+both transactions are created.
+
+=cut
+
+sub DeleteLink {
+ my $self = shift;
+ my %args = (
+ Base => undef,
+ Target => undef,
+ Type => undef,
+ Silent => undef,
+ SilentBase => undef,
+ SilentTarget => undef,
+ @_
+ );
+
+ unless ( $args{'Target'} || $args{'Base'} ) {
+ $RT::Logger->error("Base or Target must be specified");
+ return ( 0, $self->loc('Either base or target must be specified') );
+ }
+
+ #check acls
+ my $right = 0;
+ $right++ if $self->CurrentUserHasRight('ModifyTicket');
+ if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+ return (0, $msg) unless $status;
+ if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+ $right++;
+ }
+ if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+ ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+ {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+ return ( 0, $Msg ) unless $val;
+
+ return ( $val, $Msg ) if $args{'Silent'};
+
+ my ($direction, $remote_link);
+
+ if ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+ elsif ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction = 'Base';
+ }
+
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ unless ( $args{ 'Silent'. $direction } ) {
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => 'DeleteLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ OldValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+ }
+
+ if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $Msg ) = $OtherObj->_NewTransaction(
+ Type => 'DeleteLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ OldValue => $self->URI,
+ ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+ TimeTaken => 0,
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
+ }
+
+ return ( $val, $Msg );
+}
+
+
+
+=head2 AddLink
+
+Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
+
+If Silent is true then no transaction would be recorded, in other
+case you can control creation of transactions on both base and
+target with SilentBase and SilentTarget respectively. By default
+both transactions are created.
+
+=cut
+
+sub AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ SilentBase => undef,
+ SilentTarget => undef,
+ @_ );
+
+ unless ( $args{'Target'} || $args{'Base'} ) {
+ $RT::Logger->error("Base or Target must be specified");
+ return ( 0, $self->loc('Either base or target must be specified') );
+ }
+
+ my $right = 0;
+ $right++ if $self->CurrentUserHasRight('ModifyTicket');
+ if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+ return (0, $msg) unless $status;
+ if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+ $right++;
+ }
+ if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+ ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+ {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ return $self->_AddLink(%args);
+}
+
+sub __GetTicketFromURI {
+ my $self = shift;
+ my %args = ( URI => '', @_ );
+
+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my $uri_obj = RT::URI->new( $self->CurrentUser );
+ $uri_obj->FromURI( $args{'URI'} );
+
+ unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
+ my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
+ $RT::Logger->warning( $msg );
+ return( 0, $msg );
+ }
+ my $obj = $uri_obj->Resolver->Object;
+ unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
+ return (1, 'Found not a ticket', undef);
+ }
+ return (1, 'Found ticket', $obj);
+}
+
+=head2 _AddLink
+
+Private non-acled variant of AddLink so that links can be added during create.
+
+=cut
+
+sub _AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ SilentBase => undef,
+ SilentTarget => undef,
+ @_ );
+
+ my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+ return ($val, $msg) if !$val || $exist;
+ return ($val, $msg) if $args{'Silent'};
+
+ my ($direction, $remote_link);
+ if ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction = 'Base';
+ } elsif ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ unless ( $args{ 'Silent'. $direction } ) {
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => 'AddLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ NewValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+ }
+
+ if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $msg ) = $OtherObj->_NewTransaction(
+ Type => 'AddLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ NewValue => $self->URI,
+ ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+ TimeTaken => 0,
+ );
+ $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
+ }
+
+ return ( $val, $msg );
+}
+
+
+
+
+=head2 MergeInto
+
+MergeInto take the id of the ticket to merge this ticket into.
+
+=cut
+
+sub MergeInto {
+ my $self = shift;
+ my $ticket_id = shift;
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # Load up the new ticket.
+ my $MergeInto = RT::Ticket->new($self->CurrentUser);
+ $MergeInto->Load($ticket_id);
+
+ # make sure it exists.
+ unless ( $MergeInto->Id ) {
+ return ( 0, $self->loc("New ticket doesn't exist") );
+ }
+
+ # Make sure the current user can modify the new ticket.
+ unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ delete $MERGE_CACHE{'effective'}{ $self->id };
+ delete @{ $MERGE_CACHE{'merged'} }{
+ $ticket_id, $MergeInto->id, $self->id
+ };
+
+ $RT::Handle->BeginTransaction();
+
+ $self->_MergeInto( $MergeInto );
+
+ $RT::Handle->Commit();
+
+ return ( 1, $self->loc("Merge Successful") );
+}
+
+sub _MergeInto {
+ my $self = shift;
+ my $MergeInto = shift;
+
+
+ # We use EffectiveId here even though it duplicates information from
+ # the links table becasue of the massive performance hit we'd take
+ # by trying to do a separate database query for merge info everytime
+ # loaded a ticket.
+
+ #update this ticket's effective id to the new ticket's id.
+ my ( $id_val, $id_msg ) = $self->__Set(
+ Field => 'EffectiveId',
+ Value => $MergeInto->Id()
+ );
+
+ unless ($id_val) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
+ }
+
+
+ my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
+ if ( $force_status && $force_status ne $self->__Value('Status') ) {
+ my ( $status_val, $status_msg )
+ = $self->__Set( Field => 'Status', Value => $force_status );
+
+ unless ($status_val) {
+ $RT::Handle->Rollback();
+ $RT::Logger->error(
+ "Couldn't set status to $force_status. RT's Database may be inconsistent."
+ );
+ return ( 0, $self->loc("Merge failed. Couldn't set Status") );
+ }
+ }
+
+ # update all the links that point to that old ticket
+ my $old_links_to = RT::Links->new($self->CurrentUser);
+ $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
+
+ my %old_seen;
+ while (my $link = $old_links_to->Next) {
+ if (exists $old_seen{$link->Base."-".$link->Type}) {
+ $link->Delete;
+ }
+ elsif ($link->Base eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetTarget($MergeInto->URI);
+ $link->SetLocalTarget($MergeInto->id);
+ }
+ $old_seen{$link->Base."-".$link->Type} =1;
+ }
+
+ }
+
+ my $old_links_from = RT::Links->new($self->CurrentUser);
+ $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
+
+ while (my $link = $old_links_from->Next) {
+ if (exists $old_seen{$link->Type."-".$link->Target}) {
+ $link->Delete;
+ }
+ if ($link->Target eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetBase($MergeInto->URI);
+ $link->SetLocalBase($MergeInto->id);
+ $old_seen{$link->Type."-".$link->Target} =1;
+ }
+ }
+
+ }
+
+ # Update time fields
+ foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
+
+ my $mutator = "Set$type";
+ $MergeInto->$mutator(
+ ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
+
+ }
+#add all of this ticket's watchers to that ticket.
+ foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
+
+ my $people = $self->$watcher_type->MembersObj;
+ my $addwatcher_type = $watcher_type;
+ $addwatcher_type =~ s/s$//;
+
+ while ( my $watcher = $people->Next ) {
+
+ my ($val, $msg) = $MergeInto->_AddWatcher(
+ Type => $addwatcher_type,
+ Silent => 1,
+ PrincipalId => $watcher->MemberId
+ );
+ unless ($val) {
+ $RT::Logger->debug($msg);
+ }
+ }
+
+ }
+
+ #find all of the tickets that were merged into this ticket.
+ my $old_mergees = RT::Tickets->new( $self->CurrentUser );
+ $old_mergees->Limit(
+ FIELD => 'EffectiveId',
+ OPERATOR => '=',
+ VALUE => $self->Id
+ );
+
+ # update their EffectiveId fields to the new ticket's id
+ while ( my $ticket = $old_mergees->Next() ) {
+ my ( $val, $msg ) = $ticket->__Set(
+ Field => 'EffectiveId',
+ Value => $MergeInto->Id()
+ );
+ }
+
+ #make a new link: this ticket is merged into that other ticket.
+ $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
+
+ $MergeInto->_SetLastUpdated;
+}
+
+=head2 Merged
+
+Returns list of tickets' ids that's been merged into this ticket.
+
+=cut
+
+sub Merged {
+ my $self = shift;
+
+ my $id = $self->id;
+ return @{ $MERGE_CACHE{'merged'}{ $id } }
+ if $MERGE_CACHE{'merged'}{ $id };
+
+ my $mergees = RT::Tickets->new( $self->CurrentUser );
+ $mergees->Limit(
+ FIELD => 'EffectiveId',
+ VALUE => $id,
+ );
+ $mergees->Limit(
+ FIELD => 'id',
+ OPERATOR => '!=',
+ VALUE => $id,
+ );
+ return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
+ = map $_->id, @{ $mergees->ItemsArrayRef || [] };
+}
+
+
+
+
+
+=head2 OwnerObj
+
+Takes nothing and returns an RT::User object of
+this ticket's owner
+
+=cut
+
+sub OwnerObj {
+ my $self = shift;
+
+ #If this gets ACLed, we lose on a rights check in User.pm and
+ #get deep recursion. if we need ACLs here, we need
+ #an equiv without ACLs
+
+ my $owner = RT::User->new( $self->CurrentUser );
+ $owner->Load( $self->__Value('Owner') );
+
+ #Return the owner object
+ return ($owner);
+}
+
+
+
+=head2 OwnerAsString
+
+Returns the owner's email address
+
+=cut
+
+sub OwnerAsString {
+ my $self = shift;
+ return ( $self->OwnerObj->EmailAddress );
+
+}
+
+
+
+=head2 SetOwner
+
+Takes two arguments:
+ the Id or Name of the owner
+and (optionally) the type of the SetOwner Transaction. It defaults
+to 'Set'. 'Steal' is also a valid option.
+
+
+=cut
-use strict;
+sub SetOwner {
+ my $self = shift;
+ my $NewOwner = shift;
+ my $Type = shift || "Set";
+
+ $RT::Handle->BeginTransaction();
+
+ $self->_SetLastUpdated(); # lock the ticket
+ $self->Load( $self->id ); # in case $self changed while waiting for lock
+
+ my $OldOwnerObj = $self->OwnerObj;
+
+ my $NewOwnerObj = RT::User->new( $self->CurrentUser );
+ $NewOwnerObj->Load( $NewOwner );
+ unless ( $NewOwnerObj->Id ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("That user does not exist") );
+ }
+
+
+ # must have ModifyTicket rights
+ # or TakeTicket/StealTicket and $NewOwner is self
+ # see if it's a take
+ if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('TakeTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ # see if it's a steal
+ elsif ( $OldOwnerObj->Id != RT->Nobody->Id
+ && $OldOwnerObj->Id != $self->CurrentUser->id ) {
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('StealTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+ else {
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ # If we're not stealing and the ticket has an owner and it's not
+ # the current user
+ if ( $Type ne 'Steal' and $Type ne 'Force'
+ and $OldOwnerObj->Id != RT->Nobody->Id
+ and $OldOwnerObj->Id != $self->CurrentUser->Id )
+ {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("You can only take tickets that are unowned") )
+ if $NewOwnerObj->id == $self->CurrentUser->id;
+ return (
+ 0,
+ $self->loc("You can only reassign tickets that you own or that are unowned" )
+ );
+ }
+
+ #If we've specified a new owner and that user can't modify the ticket
+ elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("That user may not own tickets in that queue") );
+ }
+
+ # If the ticket has an owner and it's the new owner, we don't need
+ # To do anything
+ elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("That user already owns that ticket") );
+ }
+
+ # Delete the owner in the owner group, then add a new one
+ # TODO: is this safe? it's not how we really want the API to work
+ # for most things, but it's fast.
+ my ( $del_id, $del_msg );
+ for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
+ ($del_id, $del_msg) = $owner->Delete();
+ last unless ($del_id);
+ }
+
+ unless ($del_id) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
+ }
+
+ my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
+ PrincipalId => $NewOwnerObj->PrincipalId,
+ InsideTransaction => 1 );
+ unless ($add_id) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
+ }
+
+ # We call set twice with slightly different arguments, so
+ # as to not have an SQL transaction span two RT transactions
+
+ my ( $val, $msg ) = $self->_Set(
+ Field => 'Owner',
+ RecordTransaction => 0,
+ Value => $NewOwnerObj->Id,
+ TimeTaken => 0,
+ TransactionType => 'Set',
+ CheckACL => 0, # don't check acl
+ );
+
+ unless ($val) {
+ $RT::Handle->Rollback;
+ return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
+ }
+
+ ($val, $msg) = $self->_NewTransaction(
+ Type => 'Set',
+ Field => 'Owner',
+ NewValue => $NewOwnerObj->Id,
+ OldValue => $OldOwnerObj->Id,
+ TimeTaken => 0,
+ );
+
+ if ( $val ) {
+ $msg = $self->loc( "Owner changed from [_1] to [_2]",
+ $OldOwnerObj->Name, $NewOwnerObj->Name );
+ }
+ else {
+ $RT::Handle->Rollback();
+ return ( 0, $msg );
+ }
+
+ $RT::Handle->Commit();
+
+ return ( $val, $msg );
+}
-=head1 NAME
-RT::Ticket
+=head2 Take
+A convenince method to set the ticket's owner to the current user
-=head1 SYNOPSIS
+=cut
-=head1 DESCRIPTION
+sub Take {
+ my $self = shift;
+ return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
+}
-=head1 METHODS
+
+
+=head2 Untake
+
+Convenience method to set the owner to 'nobody' if the current user is the owner.
=cut
diff --cc lib/RT/Tickets.pm
index 35d43e9,b3a84ae..5401373
mode 100644,100755..100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@@ -1431,2109 -119,5 +1431,2108 @@@ Meta Data
=cut
+use Regexp::Common qw(RE_net_IPv4);
+use Regexp::Common::net::CIDR;
+
+
+sub _CustomFieldLimit {
+ my ( $self, $_field, $op, $value, %rest ) = @_;
+
+ my $field = $rest{'SUBKEY'} || die "No field specified";
+
+ # For our sanity, we can only limit on one queue at a time
+
+ my ($queue, $cfid, $cf, $column);
+ ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
+ $cfid = $cf ? $cf->id : 0 ;
+
+# If we're trying to find custom fields that don't match something, we
+# want tickets where the custom field has no value at all. Note that
+# we explicitly don't include the "IS NULL" case, since we would
+# otherwise end up with a redundant clause.
+
+ my ($negative_op, $null_op, $inv_op, $range_op)
+ = $self->ClassifySQLOperation( $op );
+
+ my $fix_op = sub {
+ 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' ) {
+
+ if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
+
+ # convert incomplete 192.168/24 to 192.168.0.0/24 format
+ $value =
+ join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
+ || $value;
+ }
+
+ 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");
+ }
+ }
+
+ my $single_value = !$cf || !$cfid || $cf->SingleValue;
+
+ my $cfkey = $cfid ? $cfid : "$queue.$field";
+
+ if ( $null_op && !$column ) {
+ # IS[ NOT] NULL without column is the same as has[ no] any CF value,
+ # we can reuse our default joins for this operation
+ # with column specified we have different situation
+ my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'id',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+ $self->_SQLLimit(
+ ALIAS => $CFs,
+ FIELD => 'Name',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ ) if $CFs;
+ $self->_CloseParen;
+ }
+ elsif ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
+
+ my ($start_ip, $end_ip) = split /-/, $value;
+
+ $self->_OpenParen;
+ if ( $op !~ /NOT|!=|<>/i ) { # positive equation
+ $self->_CustomFieldLimit(
+ 'CF', '<=', $end_ip, %rest,
+ SUBKEY => $rest{'SUBKEY'}. '.Content',
+ );
+ $self->_CustomFieldLimit(
+ 'CF', '>=', $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 ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+ $self->_OpenParen;
+
+ $self->_OpenParen;
+
+ $self->_OpenParen;
+ # if column is defined then deal only with it
+ # otherwise search in Content and in LargeContent
+ if ( $column ) {
+ $self->_SQLLimit( $fix_op->(
+ ALIAS => $TicketCFs,
+ FIELD => $column,
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ ) );
+ $self->_CloseParen;
+ $self->_CloseParen;
+ $self->_CloseParen;
+ }
+ else {
+ my $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->Load($field);
+
+ # need special treatment for Date
+ if ( $cf->Type eq 'DateTime' && $op eq '=' ) {
+
+ if ( $value =~ /:/ ) {
+ # there is time speccified.
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => "=",
+ VALUE => $date->ISO,
+ %rest,
+ );
+ }
+ else {
+ # 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 );
+ $date->SetToMidnight( Timezone => 'server' );
+ my $daystart = $date->ISO;
+ $date->AddDay;
+ my $dayend = $date->ISO;
+
+ $self->_OpenParen;
+
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ %rest,
+ );
+
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ 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 => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+ }
+ else {
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit( $fix_op->(
+ ALIAS => $TicketCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ ) );
+ }
+ }
+ else {
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest
+ );
+
+ $self->_OpenParen;
+ $self->_OpenParen;
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => '=',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'OR'
+ );
+ $self->_CloseParen;
+ $self->_SQLLimit( $fix_op->(
+ ALIAS => $TicketCFs,
+ FIELD => 'LargeContent',
+ OPERATOR => $op,
+ VALUE => $value,
+ ENTRYAGGREGATOR => 'AND',
+ ) );
+ $self->_CloseParen;
+ }
+ $self->_CloseParen;
+
+ # XXX: if we join via CustomFields table then
+ # because of order of left joins we get NULLs in
+ # CF table and then get nulls for those records
+ # in OCFVs table what result in wrong results
+ # as decifer method now tries to load a CF then
+ # we fall into this situation only when there
+ # are more than one CF with the name in the DB.
+ # the same thing applies to order by call.
+ # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
+ # we want treat IS NULL as (not applies or has
+ # no value)
+ $self->_SQLLimit(
+ ALIAS => $CFs,
+ FIELD => 'Name',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ ) if $CFs;
+ $self->_CloseParen;
+
+ if ($negative_op) {
+ $self->_SQLLimit(
+ ALIAS => $TicketCFs,
+ FIELD => $column || 'Content',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'OR',
+ );
+ }
+
+ $self->_CloseParen;
+ }
+ }
+ else {
+ $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
+ my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+ # 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 => $TicketCFs,
+ ALIAS => $TicketCFs,
+ FIELD => $column,
+ OPERATOR => $op,
+ VALUE => $value,
+ ) );
+ }
+ else {
+ $self->SUPER::Limit(
+ LEFTJOIN => $TicketCFs,
+ ALIAS => $TicketCFs,
+ FIELD => 'Content',
+ OPERATOR => $op,
+ VALUE => $value,
+ );
+ }
+ $self->_SQLLimit(
+ %rest,
+ ALIAS => $TicketCFs,
+ 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 ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
+ my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
+ $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
+ my ($TicketCFs, $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 => $TicketCFs,
+ FIELD1 => 'CustomField',
+ TABLE2 => 'CustomFieldValues',
+ FIELD2 => 'CustomField',
+ );
+ $self->SUPER::Limit(
+ LEFTJOIN => $CFvs,
+ FIELD => 'Name',
+ QUOTEVALUE => 0,
+ VALUE => $TicketCFs . ".Content",
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
+ push @res, { %$row, ALIAS => $TicketCFs, 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 } ;
+ }
+ else {
+ push @res, $row;
+ }
+ }
+ return $self->SUPER::OrderByCols(@res);
+}
+
+
+
+
+=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{'Limit'}, ),
+ );
+}
+
+
+
+
+
+=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 at 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 ( $row, %listing );
++ my %listing;
+
- foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
++ 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 $row;
+ my %clause;
- foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
++ 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")
+
+ 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;
-----------------------------------------------------------------------
More information about the Rt-commit
mailing list