From 3ef62a0570055da710328937e7f65dbb2c027c62 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 12 Aug 2002 06:17:09 +0000 Subject: import rt 2.0.14 --- rt/lib/RT/Ticket.pm | 3004 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3004 insertions(+) create mode 100755 rt/lib/RT/Ticket.pm (limited to 'rt/lib/RT/Ticket.pm') diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm new file mode 100755 index 000000000..f7275e4e3 --- /dev/null +++ b/rt/lib/RT/Ticket.pm @@ -0,0 +1,3004 @@ +# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ +# (c) 1996-2001 Jesse Vincent +# This software is redistributable under the terms of the GNU GPL +# + +=head1 NAME + + RT::Ticket - RT ticket object + +=head1 SYNOPSIS + + use RT::Ticket; + my $ticket = new RT::Ticket($CurrentUser); + $ticket->Load($ticket_id); + +=head1 DESCRIPTION + +This module lets you manipulate RT\'s ticket object. + + +=head1 METHODS + +=cut + + + +package RT::Ticket; +use RT::Queue; +use RT::User; +use RT::Record; +use RT::Link; +use RT::Links; +use RT::Date; +use RT::Watcher; + + +@ISA= qw(RT::Record); + + +=begin testing + +use RT::TestHarness; + +ok(require RT::Ticket, "Loading the RT::Ticket library"); + +=end testing + +=cut + +# {{{ sub _Init + +sub _Init { + my $self = shift; + $self->{'table'} = "Tickets"; + return ($self->SUPER::_Init(@_)); +} + +# }}} + +# {{{ sub Load + +=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; + + #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. + + #If it's a local URI, turn it into a ticket id + if ($id =~ /^$RT::TicketBaseURI(\d+)$/) { + $id = $1; + } + #If it's a remote URI, we're going to punt for now + elsif ($id =~ '://' ) { + return (undef); + } + + #If we have an integer URI, load the ticket + if ( $id =~ /^\d+$/ ) { + my $ticketid = $self->LoadById($id); + + unless ($ticketid) { + $RT::Logger->debug("$self tried to load a bogus ticket: $id\n"); + return(undef); + } + } + + #It's not a URI. It's not a numerical ticket ID. Punt! + else { + return(undef); + } + + #If we're merged, resolve the merge. + if (($self->EffectiveId) and + ($self->EffectiveId != $self->Id)) { + return ($self->Load($self->EffectiveId)); + } + + #Ok. we're loaded. lets get outa here. + return ($self->Id); + +} + +# }}} + +# {{{ sub LoadByURI + +=head2 LoadByURI + +Given a local ticket URI, loads the specified ticket. + +=cut + +sub LoadByURI { + my $self = shift; + my $uri = shift; + + if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) { + my $id = $1; + return ($self->Load($id)); + } + else { + return(undef); + } +} + +# }}} + +# {{{ sub Create + +=head2 Create (ARGS) + +Arguments: ARGS is a hash of named parameters. Valid parameters are: + + Queue - Either a Queue object or a Queue Name + Requestor - A reference to a list of RT::User objects, email addresses or RT user Names + Cc - A reference to a list of RT::User objects, email addresses or Names + AdminCc - A reference to a list of RT::User objects, email addresses or Names + 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 + InitialPriority -- an integer from 0 to 99 + FinalPriority -- an integer from 0 to 99 + Status -- any valid status (Defined in RT::Queue) + TimeWorked -- an integer + TimeLeft -- an integer + 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. + + KeywordSelect- -- an array of keyword ids for that keyword select + + +Returns: TICKETID, Transaction Object, Error Message + + +=begin testing + +my $t = RT::Ticket->new($RT::SystemUser); + +ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created"); + +ok ( my $id = $t->Id, "Got ticket id"); + +=end testing + +=cut + +sub Create { + my $self = shift; + + my %args = ( + Queue => undef, + Requestor => undef, + Cc => undef, + AdminCc => undef, + Type => 'ticket', + Owner => $RT::Nobody->UserObj, + Subject => '[no subject]', + InitialPriority => undef, + FinalPriority => undef, + Status => 'new', + TimeWorked => "0", + TimeLeft => 0, + Due => undef, + Starts => undef, + MIMEObj => undef, + @_); + + my ($ErrStr, $QueueObj, $Owner, $resolved); + my (@non_fatal_errors); + + my $now = RT::Date->new($self->CurrentUser); + $now->SetToNow(); + + if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) { + $QueueObj=RT::Queue->new($RT::SystemUser); + $QueueObj->Load($args{'Queue'}); + } + 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)) { + $RT::Logger->debug( "$self No queue given for ticket creation."); + return (0, 0,'Could not create ticket. Queue not set'); + } + + #Now that we have a queue, Check the ACLS + unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', + QueueObj => $QueueObj )) { + return (0,0,"No permission to create tickets in the queue '". + $QueueObj->Name."'."); + } + + #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'}); + + + #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 = new RT::Date($self->CurrentUser); + if (defined $args{'Due'}) { + $due->Set (Format => 'ISO', + Value => $args{'Due'}); + } + elsif (defined ($QueueObj->DefaultDueIn)) { + $due->SetToNow; + $due->AddDays($QueueObj->DefaultDueIn); + } + + my $starts = new RT::Date($self->CurrentUser); + if (defined $args{'Starts'}) { + $starts->Set (Format => 'ISO', + Value => $args{'Starts'}); + } + + + # {{{ Deal with setting the owner + + if (ref($args{'Owner'}) eq 'RT::User') { + $Owner = $args{'Owner'}; + } + #If we've been handed something else, try to load the user. + elsif ($args{'Owner'}) { + $Owner = new RT::User($self->CurrentUser); + $Owner->Load($args{'Owner'}); + + } + #If we can't handle it, call it nobody + else { + if (ref($args{'Owner'})) { + $RT::Logger->warning("$ticket ->Create called with an Owner of ". + "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n"); + + push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; + } + else { + $RT::Logger->warning("$self ->Create called with an ". + "unknown datatype for Owner: ".$args{'Owner'} . + ". Defaulting to Nobody.\n"); + } + } + + #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->HasQueueRight( QueueObj => $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 this queue\n"); + + push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; + + $Owner = undef; + } + + #If we haven't been handed a valid owner, make it nobody. + unless (defined ($Owner)) { + $Owner = new RT::User($self->CurrentUser); + $Owner->Load($RT::Nobody->UserObj->Id); + } + + # }}} + + unless ($self->ValidateStatus($args{'Status'})) { + return (0,0,'Invalid value for status'); + } + + if ($args{'Status'} eq 'resolved') { + $resolved = $now->ISO; + } else{ + $resolved = undef; + } + + my $id = $self->SUPER::Create( + Queue => $QueueObj->Id, + Owner => $Owner->Id, + Subject => $args{'Subject'}, + InitialPriority => $args{'InitialPriority'}, + FinalPriority => $args{'FinalPriority'}, + Priority => $args{'InitialPriority'}, + Status => $args{'Status'}, + TimeWorked => $args{'TimeWorked'}, + TimeLeft => $args{'TimeLeft'}, + Type => $args{'Type'}, + Starts => $starts->ISO, + Resolved => $resolved, + Due => $due->ISO + ); + #Set the ticket's effective ID now that we've created it. + my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id); + + unless ($val) { + $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n"); + } + + + my $watcher; + foreach $watcher (@{$args{'Cc'}}) { + my ($wval, $wmsg) = + $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); + push @non_fatal_errors, $wmsg unless ($wval); + } + + foreach $watcher (@{$args{'Requestor'}}) { + my ($wval, $wmsg) = + $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); + push @non_fatal_errors, $wmsg unless ($wval); + } + + foreach $watcher (@{$args{'AdminCc'}}) { + # 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 ($wval, $wmsg) = + $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); + push @non_fatal_errors, $wmsg unless ($wval); + } + + # Iterate through all the KeywordSelect- params passed in, calling _AddKeyword + # for each of them + + + foreach my $key (keys %args) { + + next unless ($key =~ /^KeywordSelect-(.*)$/); + + my $ks = $1; + + + my @keywords = ref($args{$key}) eq 'ARRAY' ? + @{$args{$key}} : ($args{$key}); + + foreach my $keyword (@keywords) { + my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks, + Keyword => $keyword, + Silent => 1); + } + push @non_fatal_errors, $kmsg unless ($kval); + } + + + + #Add a transaction for the create + my ($Trans, $Msg, $TransObj) = + $self->_NewTransaction( Type => "Create", + TimeTaken => 0, + MIMEObj=>$args{'MIMEObj'}); + + # Logging + if ($self->Id && $Trans) { + $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name. + "'.\n" . join("\n", @non_fatal_errors); + + $RT::Logger->info($ErrStr); + } + else { + # TODO where does this get errstr from? + $RT::Logger->warning("Ticket couldn't be created: $ErrStr"); + } + + return($self->Id, $TransObj->Id, $ErrStr); +} + +# }}} + +# {{{ sub Import + +=head2 Import PARAMHASH + +Import a ticket. +Doesn\'t create a transaction. +Doesn\'t supply queue defaults, etc. + +Arguments are identical to Create(), with the addition of + Id - Ticket Id + +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,'Could not create ticket. Queue not set'); + } + + #Now that we have a queue, Check the ACLS + unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', + QueueObj => $QueueObj )) { + return (0,"No permission to create tickets in the queue '". + $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 = new RT::User($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->HasQueueRight( QueueObj => $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."'\n"); + + $Owner = undef; + } + + #If we haven't been handed a valid owner, make it nobody. + unless (defined ($Owner)) { + $Owner = new RT::User($self->CurrentUser); + $Owner->Load($RT::Nobody->UserObj->Id); + } + + # }}} + + unless ($self->ValidateStatus($args{'Status'})) { + return (0,"'$args{'Status'}' is an invalid value for 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'}, + InitialPriority => $args{'InitialPriority'}, + FinalPriority => $args{'FinalPriority'}, + Priority => $args{'InitialPriority'}, + Status => $args{'Status'}, + TimeWorked => $args{'TimeWorked'}, + Type => $args{'Type'}, + Created => $args{'Created'}, + Told => $args{'Told'}, + LastUpdated => $args{'Updated'}, + Resolved => $args{Resolved}, + Due => $args{'Due'}, + ); + + + + # 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\n"); + } + } + + my $watcher; + foreach $watcher (@{$args{'Cc'}}) { + $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); + } + foreach $watcher (@{$args{'AdminCc'}}) { + $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); + } + foreach $watcher (@{$args{'Requestor'}}) { + $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); + } + + return($self->Id, $ErrStr); +} + +# }}} + +# {{{ sub Delete + +sub Delete { + my $self = shift; + return (0, 'Deleting this object would violate referential integrity.'. + ' That\'s bad.'); +} +# }}} + +# {{{ Routines dealing with watchers. + +# {{{ Routines dealing with adding new watchers + +# {{{ sub AddWatcher + +=head2 AddWatcher + +AddWatcher takes a parameter hash. The keys are as follows: + +Email +Type +Owner + +If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address. + +=cut + +sub AddWatcher { + my $self = shift; + my %args = ( Email => undef, + Type => undef, + Owner => undef, + @_ + ); + + # {{{ Check ACLS + #If the watcher we're trying to add is for the current user + if ( ( $self->CurrentUser->EmailAddress && + ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or + ($args{'Owner'} eq $self->CurrentUser->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, '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, 'Permission Denied'); + } + } + else { + $RT::Logger->warn("$self -> AddWatcher hit code". + " it never should. We got passed ". + " a type of ". $args{'Type'}); + return (0,'Error in parameters to TicketAddWatcher'); + } + } + # If the watcher isn't the current user + # and the current user doesn't have 'ModifyTicket' + # bail + else { + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + } + # }}} + + 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, + Email => undef, + Owner => 0, + Person => undef, + @_ ); + + + + #clear the watchers cache + $self->{'watchers_cache'} = undef; + + if (defined $args{'Person'}) { + #if it's an RT::User object, pull out the id and shove it in Owner + if (ref ($args{'Person'}) =~ /RT::User/) { + $args{'Owner'} = $args{'Person'}->id; + } + #if it's an int, shove it in Owner + elsif ($args{'Person'} =~ /^\d+$/) { + $args{'Owner'} = $args{'Person'}; + } + #if it's an email address, shove it in Email + else { + $args{'Email'} = $args{'Person'}; + } + } + + # Turn an email address int a watcher if we possibly can. + if ($args{'Email'}) { + my $watcher = new RT::User($self->CurrentUser); + $watcher->LoadByEmail($args{'Email'}); + if ($watcher->Id) { + $args{'Owner'} = $watcher->Id; + delete $args{'Email'}; + } + } + + + # see if this user is already a watcher. if we have an owner, check it + # otherwise, we've got an email-address watcher. use that. + + if ($self->IsWatcher(Type => $args{'Type'}, + Id => ($args{'Owner'} || $args{'Email'}) ) ) { + + + return(0, 'That user is already that sort of watcher for this ticket'); + } + + + require RT::Watcher; + my $Watcher = new RT::Watcher ($self->CurrentUser); + my ($retval, $msg) = ($Watcher->Create( Value => $self->Id, + Scope => 'Ticket', + Email => $args{'Email'}, + Type => $args{'Type'}, + Owner => $args{'Owner'}, + )); + + unless ($args{'Silent'}) { + $self->_NewTransaction( Type => 'AddWatcher', + NewValue => $Watcher->Email, + Field => $Watcher->Type); + } + + return ($retval, $msg); +} + +# }}} + +# {{{ sub AddRequestor + +=head2 AddRequestor + +AddRequestor takes what AddWatcher does, except it presets +the "Type" parameter to \'Requestor\' + +=cut + +sub AddRequestor { + my $self = shift; + return ($self->AddWatcher ( Type => 'Requestor', @_)); +} + +# }}} + +# {{{ sub AddCc + +=head2 AddCc + +AddCc takes what AddWatcher does, except it presets +the "Type" parameter to \'Cc\' + +=cut + +sub AddCc { + my $self = shift; + return ($self->AddWatcher ( Type => 'Cc', @_)); +} +# }}} + +# {{{ sub AddAdminCc + +=head2 AddAdminCc + +AddAdminCc takes what AddWatcher does, except it presets +the "Type" parameter to \'AdminCc\' + +=cut + +sub AddAdminCc { + my $self = shift; + return ($self->AddWatcher ( Type => 'AdminCc', @_)); +} + +# }}} + +# }}} + +# {{{ sub DeleteWatcher + +=head2 DeleteWatcher id [type] + +DeleteWatcher takes a single argument which is either an email address +or a watcher id. +If the first argument is an email address, you need to specify the watcher type you're talking +about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'. +It removes that watcher from this Ticket\'s list of watchers. + + +=cut + +#TODO It is lame that you can't call this the same way you can call AddWatcher + +sub DeleteWatcher { + my $self = shift; + my $id = shift; + + my $type; + + $type = shift if (@_); + + my $Watcher = new RT::Watcher($self->CurrentUser); + + #If it\'s a numeric watcherid + if ($id =~ /^(\d*)$/) { + $Watcher->Load($id); + } + + #Otherwise, we'll assume it's an email address + elsif ($type) { + my ($result, $msg) = + $Watcher->LoadByValue( Email => $id, + Scope => 'Ticket', + Value => $self->id, + Type => $type); + return (0,$msg) unless ($result); + } + + else { + return(0,"Can\'t delete a watcher by email address without specifying a type"); + } + + # {{{ Check ACLS + + #If the watcher we're trying to delete is for the current user + if ($Watcher->Email eq $self->CurrentUser->EmailAddress) { + + # If it's an AdminCc and they don't have + # 'WatchAsAdminCc' or 'ModifyTicket', bail + if ($Watcher->Type eq 'AdminCc') { + unless ($self->CurrentUserHasRight('ModifyTicket') or + $self->CurrentUserHasRight('WatchAsAdminCc')) { + return(0, 'Permission Denied'); + } + } + + # If it's a Requestor or Cc and they don't have + # 'Watch' or 'ModifyTicket', bail + elsif (($Watcher->Type eq 'Cc') or + ($Watcher->Type eq 'Requestor')) { + + unless ($self->CurrentUserHasRight('ModifyTicket') or + $self->CurrentUserHasRight('Watch')) { + return(0, 'Permission Denied'); + } + } + else { + $RT::Logger->warn("$self -> DeleteWatcher hit code". + " it never should. We got passed ". + " a type of ". $args{'Type'}); + return (0,'Error in parameters to $self 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, "Permission Denied"); + } + } + + # }}} + + unless (($Watcher->Scope eq 'Ticket') and + ($Watcher->Value == $self->id) ) { + return (0, "Not a watcher for this ticket"); + } + + + #Clear out the watchers hash. + $self->{'watchers'} = undef; + + #If we\'ve validated that it is a watcher for this ticket + $self->_NewTransaction ( Type => 'DelWatcher', + OldValue => $Watcher->Email, + Field => $Watcher->Type, + ); + + my $retval = $Watcher->Delete(); + + unless ($retval) { + return(0,"Watcher could not be deleted. Database inconsistency possible."); + } + + return(1, "Watcher deleted"); +} + +# {{{ sub DeleteRequestor + +=head2 DeleteRequestor EMAIL + +Takes an email address. It calls DeleteWatcher with a preset +type of 'Requestor' + + +=cut + +sub DeleteRequestor { + my $self = shift; + my $id = shift; + return ($self->DeleteWatcher ($id, 'Requestor')) +} + +# }}} + +# {{{ sub DeleteCc + +=head2 DeleteCc EMAIL + +Takes an email address. It calls DeleteWatcher with a preset +type of 'Cc' + + +=cut + +sub DeleteCc { + my $self = shift; + my $id = shift; + return ($self->DeleteWatcher ($id, 'Cc')) +} + +# }}} + +# {{{ sub DeleteAdminCc + +=head2 DeleteAdminCc EMAIL + +Takes an email address. It calls DeleteWatcher with a preset +type of 'AdminCc' + + +=cut + +sub DeleteAdminCc { + my $self = shift; + my $id = shift; + return ($self->DeleteWatcher ($id, 'AdminCc')) +} + +# }}} + + +# }}} + +# {{{ sub Watchers + +=head2 Watchers + +Watchers returns a Watchers object preloaded with this ticket\'s watchers. + +# It should return only the ticket watchers. the actual FooAsString +# methods capture the queue watchers too. I don't feel thrilled about this, +# but we don't want the Cc Requestors and AdminCc objects to get filled up +# with all the queue watchers too. we've got seperate objects for that. + # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch? + +=cut + +sub Watchers { + my $self = shift; + + require RT::Watchers; + my $watchers=RT::Watchers->new($self->CurrentUser); + if ($self->CurrentUserHasRight('ShowTicket')) { + $watchers->LimitToTicket($self->id); + } + + return($watchers); + +} + +# }}} + +# {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string + +=head2 RequestorsAsString + + B String: All Ticket Requestor email addresses as a string. + +=cut + +sub RequestorsAsString { + my $self=shift; + + unless ($self->CurrentUserHasRight('ShowTicket')) { + return undef; + } + + return ($self->Requestors->EmailsAsString() ); +} + +=head2 WatchersAsString + +B String: All Ticket Watchers email addresses as a string + +=cut + +sub WatchersAsString { + my $self=shift; + + unless ($self->CurrentUserHasRight('ShowTicket')) { + return (0, "Permission Denied"); + } + + return ($self->Watchers->EmailsAsString()); + +} + +=head2 AdminCcAsString + +returns String: All Ticket AdminCc email addresses as a string + +=cut + + +sub AdminCcAsString { + my $self=shift; + + unless ($self->CurrentUserHasRight('ShowTicket')) { + return undef; + } + + return ($self->AdminCc->EmailsAsString()); + +} + +=head2 CcAsString + +returns String: All Ticket Ccs as a string of email addresses + +=cut + +sub CcAsString { + my $self=shift; + + unless ($self->CurrentUserHasRight('ShowTicket')) { + return undef; + } + + return ($self->Cc->EmailsAsString()); + +} + +# }}} + +# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs + +# {{{ sub Requestors + +=head2 Requestors + +Takes nothing. +Returns this ticket's Requestors as an RT::Watchers object + +=cut + +sub Requestors { + my $self = shift; + + my $requestors = $self->Watchers(); + if ($self->CurrentUserHasRight('ShowTicket')) { + $requestors->LimitToRequestors(); + } + + return($requestors); + +} + +# }}} + +# {{{ sub Cc + +=head2 Cc + +Takes nothing. +Returns a watchers object which contains this ticket's Cc watchers + +=cut + +sub Cc { + my $self = shift; + + my $cc = $self->Watchers(); + + if ($self->CurrentUserHasRight('ShowTicket')) { + $cc->LimitToCc(); + } + + return($cc); + +} + +# }}} + +# {{{ sub AdminCc + +=head2 AdminCc + +Takes nothing. +Returns this ticket\'s administrative Ccs as an RT::Watchers object + +=cut + +sub AdminCc { + my $self = shift; + + my $admincc = $self->Watchers(); + if ($self->CurrentUserHasRight('ShowTicket')) { + $admincc->LimitToAdminCc(); + } + return($admincc); +} + +# }}} + +# }}} + +# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc + +# {{{ sub IsWatcher +# a generic routine to be called by IsRequestor, IsCc and IsAdminCc + +=head2 IsWatcher + +Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string +is a ticket watcher. Returns undef otherwise + +=cut + +sub IsWatcher { + my $self = shift; + + my %args = ( Type => 'Requestor', + Email => undef, + Id => undef, + @_ + ); + + my %cols = ('Type' => $args{'Type'}, + 'Scope' => 'Ticket', + 'Value' => $self->Id, + 'Owner' => undef, + 'Email' => undef + ); + + if (ref($args{'Id'})){ + #If it's a ref, it's an RT::User object; + $cols{'Owner'} = $args{'Id'}->Id; + } + elsif ($args{'Id'} =~ /^\d+$/) { + # if it's an integer, it's a reference to an RT::User obj + $cols{'Owner'} = $args{'Id'}; + } + else { + $cols{'Email'} = $args{'Id'}; + } + + if ($args{'Email'}) { + $cols{'Email'} = $args{'Email'}; + } + + my $description = join(":",%cols); + + #If we've cached a positive match... + if (defined $self->{'watchers_cache'}->{"$description"}) { + if ($self->{'watchers_cache'}->{"$description"} == 1) { + return(1); + } + else { #If we've cached a negative match... + return(undef); + } + } + + + my $watcher = new RT::Watcher($self->CurrentUser); + $watcher->LoadByCols(%cols); + + + if ($watcher->id) { + $self->{'watchers_cache'}->{"$description"} = 1; + return(1); + } + else { + $self->{'watchers_cache'}->{"$description"} = 0; + return(undef); + } + +} +# }}} + +# {{{ sub IsRequestor + +=head2 IsRequestor + + Takes an email address, RT::User object or integer (RT user id) + Returns true if the string is a requestor of the current ticket. + + +=cut + +sub IsRequestor { + my $self = shift; + my $person = shift; + + return ($self->IsWatcher(Type => 'Requestor', Id => $person)); + +}; + +# }}} + +# {{{ sub IsCc + +=head2 IsCc + +Takes a string. Returns true if the string is a Cc watcher of the current ticket. + +=cut + +sub IsCc { + my $self = shift; + my $cc = shift; + + return ($self->IsWatcher( Type => 'Cc', Id => $cc )); + +} + +# }}} + +# {{{ sub IsAdminCc + +=head2 IsAdminCc + +Takes a string. Returns true if the string is an AdminCc watcher of the current ticket. + +=cut + +sub IsAdminCc { + my $self = shift; + my $person = shift; + + return ($self->IsWatcher( Type => 'AdminCc', Id => $person )); + +} + +# }}} + +# {{{ sub IsOwner + +=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); + } +} + + +# }}} + +# }}} + +# }}} + +# {{{ Routines dealing with queues + +# {{{ sub ValidateQueue + +sub ValidateQueue { + my $self = shift; + my $Value = shift; + + #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue, + 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 + +sub SetQueue { + my $self = shift; + my $NewQueue = shift; + + #Redundant. ACL gets checked in _Set; + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + + my $NewQueueObj = RT::Queue->new($self->CurrentUser); + $NewQueueObj->Load($NewQueue); + + unless ($NewQueueObj->Id()) { + return (0, "That queue does not exist"); + } + + if ($NewQueueObj->Id == $self->QueueObj->Id) { + return (0, 'That is the same value'); + } + unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket', + QueueObj => $NewQueueObj )) { + return (0, "You may not create requests in that queue."); + } + + unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket', + QueueObj => $NewQueueObj)) { + $self->Untake(); + } + + return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id())); + +} + +# }}} + +# {{{ sub QueueObj + +=head2 QueueObj + +Takes nothing. returns this ticket's queue object + +=cut + +sub QueueObj { + my $self = shift; + + my $queue_obj = RT::Queue->new($self->CurrentUser); + #We call __Value so that we can avoid the ACL decision and some deep recursion + my ($result) = $queue_obj->Load($self->__Value('Queue')); + return ($queue_obj); +} + + +# }}} + +# }}} + +# {{{ Date printing routines + +# {{{ sub DueObj + +=head2 DueObj + + Returns an RT::Date object containing this ticket's due date + +=cut +sub DueObj { + my $self = shift; + + my $time = new RT::Date($self->CurrentUser); + + # -1 is RT::Date slang for never + if ($self->Due) { + $time->Set(Format => 'sql', Value => $self->Due ); + } + else { + $time->Set(Format => 'unix', Value => -1); + } + + return $time; +} +# }}} + +# {{{ sub DueAsString + +=head2 DueAsString + +Returns this ticket's due date as a human readable string + +=cut + +sub DueAsString { + my $self = shift; + return $self->DueObj->AsString(); +} + +# }}} + +# {{{ sub GraceTimeAsString + +=head2 GraceTimeAsString + +Return the time until this ticket is due as a string + +=cut + +# TODO This should be deprecated + +sub GraceTimeAsString { + my $self=shift; + + if ($self->Due) { + return ($self->DueObj->AgeAsString()); + } else { + return ""; + } +} + +# }}} + + +# {{{ sub ResolvedObj + +=head2 ResolvedObj + + Returns an RT::Date object of this ticket's 'resolved' time. + +=cut + +sub ResolvedObj { + my $self = shift; + + my $time = new RT::Date($self->CurrentUser); + $time->Set(Format => 'sql', Value => $self->Resolved); + return $time; +} +# }}} + +# {{{ sub SetStarted + +=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, "Permission Denied"); + } + + #We create a date object to catch date weirdness + my $time_obj = new RT::Date($self->CurrentUser()); + if ($time != 0) { + $time_obj->Set(Format => 'ISO', Value => $time); + } + else { + $time_obj->SetToNow(); + } + + #Now that we're starting, open this ticket + #TODO do we really want to force this as policy? it should be a scrip + + #We need $TicketAsSystem, in case the current user doesn't have + #ShowTicket + # + my $TicketAsSystem = new RT::Ticket($RT::SystemUser); + $TicketAsSystem->Load($self->Id); + if ($TicketAsSystem->Status eq 'new') { + $TicketAsSystem->Open(); + } + + return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO)); + +} + +# }}} + +# {{{ sub StartedObj + +=head2 StartedObj + + Returns an RT::Date object which contains this ticket's +'Started' time. + +=cut + + +sub StartedObj { + my $self = shift; + + my $time = new RT::Date($self->CurrentUser); + $time->Set(Format => 'sql', Value => $self->Started); + return $time; +} +# }}} + +# {{{ sub StartsObj + +=head2 StartsObj + + Returns an RT::Date object which contains this ticket's +'Starts' time. + +=cut + +sub StartsObj { + my $self = shift; + + my $time = new RT::Date($self->CurrentUser); + $time->Set(Format => 'sql', Value => $self->Starts); + return $time; +} +# }}} + +# {{{ sub ToldObj + +=head2 ToldObj + + Returns an RT::Date object which contains this ticket's +'Told' time. + +=cut + + +sub ToldObj { + my $self = shift; + + my $time = new RT::Date($self->CurrentUser); + $time->Set(Format => 'sql', Value => $self->Told); + return $time; +} + +# }}} + +# {{{ sub LongSinceToldAsString + +# TODO this should be deprecated + + +sub LongSinceToldAsString { + my $self = shift; + + if ($self->Told) { + return $self->ToldObj->AgeAsString(); + } else { + return "Never"; + } +} +# }}} + +# {{{ sub ToldAsString + +=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"); + } +} +# }}} + +# {{{ sub TimeWorkedAsString + +=head2 TimeWorkedAsString + +Returns the amount of time worked on this ticket as a Text String + +=cut + +sub TimeWorkedAsString { + my $self=shift; + return "0" unless $self->TimeWorked; + + #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. + + my $worked = new RT::Date($self->CurrentUser); + #return the #of minutes worked turned into seconds and written as + # a simple text string + + return($worked->DurationAsString($self->TimeWorked*60)); +} + +# }}} + + +# }}} + +# {{{ Routines dealing with correspondence/comments + +# {{{ sub Comment + +=head2 Comment + +Comment on this ticket. +Takes a hashref with the follwoing attributes: + +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo + +=cut + +sub Comment { + my $self = shift; + + my %args = ( + CcMessageTo => undef, + BccMessageTo => undef, + MIMEObj => undef, + TimeTaken => 0, + @_ ); + + unless (($self->CurrentUserHasRight('CommentOnTicket')) or + ($self->CurrentUserHasRight('ModifyTicket'))) { + return (0, "Permission Denied"); + } + + unless ($args{'MIMEObj'}) { + return(0,"No correspondence attached"); + } + + # 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 + + $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); + $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); + + #Record the correspondence (write the transaction) + my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment', + Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'), + TimeTaken => $args{'TimeTaken'}, + MIMEObj => $args{'MIMEObj'} + ); + + + return ($Trans, "The comment has been recorded"); +} + +# }}} + +# {{{ sub Correspond + +=head2 Correspond + +Correspond on this ticket. +Takes a hashref with the following attributes: + + +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo + +=cut + +sub Correspond { + my $self = shift; + my %args = ( + CcMessageTo => undef, + BccMessageTo => undef, + MIMEObj => undef, + TimeTaken => 0, + @_ ); + + unless (($self->CurrentUserHasRight('ReplyToTicket')) or + ($self->CurrentUserHasRight('ModifyTicket'))) { + return (0, "Permission Denied"); + } + + unless ($args{'MIMEObj'}) { + return(0,"No correspondence attached"); + } + + # 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 + + $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); + $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); + + #Record the correspondence (write the transaction) + my ($Trans,$msg, $TransObj) = $self->_NewTransaction + (Type => 'Correspond', + Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'), + TimeTaken => $args{'TimeTaken'}, + MIMEObj=> $args{'MIMEObj'} + ); + + # TODO this bit of logic should really become a scrip for 2.2 + my $TicketAsSystem = new RT::Ticket($RT::SystemUser); + $TicketAsSystem->Load($self->Id); + + if ( + ($TicketAsSystem->Status ne 'open') and + ($TicketAsSystem->Status ne 'new') + ) { + + my $oldstatus = $TicketAsSystem->Status(); + $TicketAsSystem->__Set(Field => 'Status', Value => 'open'); + $TicketAsSystem->_NewTransaction + ( Type => 'Set', + Field => 'Status', + OldValue => $oldstatus, + NewValue => 'open', + Data => 'Ticket auto-opened on incoming correspondence' + ); + } + + unless ($Trans) { + $RT::Logger->err("$self couldn't init a transaction ($msg)\n"); + return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'}); + } + + #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 ($TransObj->IsInbound) { + $self->_SetTold; + } + + return ($Trans, "correspondence sent"); +} + +# }}} + +# }}} + +# {{{ Routines dealing with Links and Relations between tickets + +# {{{ Link Collections + +# {{{ sub Members + +=head2 Members + + This returns an RT::Links object which references all the tickets +which are 'MembersOf' this ticket + +=cut + +sub Members { + my $self = shift; + return ($self->_Links('Target', 'MemberOf')); +} + +# }}} + +# {{{ sub MemberOf + +=head2 MemberOf + + This returns an RT::Links object which references all the tickets that this +ticket is a 'MemberOf' + +=cut + +sub MemberOf { + my $self = shift; + return ($self->_Links('Base', 'MemberOf')); +} + +# }}} + +# {{{ RefersTo + +=head2 RefersTo + + This returns an RT::Links object which shows all references for which this ticket is a base + +=cut + +sub RefersTo { + my $self = shift; + return ($self->_Links('Base', 'RefersTo')); +} + +# }}} + +# {{{ ReferredToBy + +=head2 ReferredToBy + + This returns an RT::Links object which shows all references for which this ticket is a target + +=cut + +sub ReferredToBy { + my $self = shift; + return ($self->_Links('Target', 'RefersTo')); +} + +# }}} + +# {{{ DependedOnBy + +=head2 DependedOnBy + + This returns an RT::Links object which references all the tickets that depend on this one + +=cut +sub DependedOnBy { + my $self = shift; + return ($self->_Links('Target','DependsOn')); +} + +# }}} + +# {{{ DependsOn + +=head2 DependsOn + + This returns an RT::Links object which references all the tickets that this ticket depends on + +=cut +sub DependsOn { + my $self = shift; + return ($self->_Links('Base','DependsOn')); +} + +# }}} + +# {{{ sub _Links + +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 || ""; + + unless ($self->{"$field$type"}) { + $self->{"$field$type"} = new RT::Links($self->CurrentUser); + if ($self->CurrentUserHasRight('ShowTicket')) { + + $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI); + $self->{"$field$type"}->Limit(FIELD=>'Type', + VALUE=>$type) if ($type); + } + } + return ($self->{"$field$type"}); +} + +# }}} + +# }}} + + +# {{{ sub DeleteLink + +=head2 DeleteLink + +Delete a link. takes a paramhash of Base, Target and Type. +Either Base or Target must be null. The null value will +be replaced with this ticket\'s id + +=cut + +sub DeleteLink { + my $self = shift; + my %args = ( Base => undef, + Target => undef, + Type => undef, + @_ ); + + #check acls + unless ($self->CurrentUserHasRight('ModifyTicket')) { + $RT::Logger->debug("No permission to delete links\n"); + return (0, 'Permission Denied'); + + + } + + #we want one of base and target. we don't care which + #but we only want _one_ + + if ($args{'Base'} and $args{'Target'}) { + $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n"); + return (0, 'Can\'t specifiy both base and target'); + } + elsif ($args{'Base'}) { + $args{'Target'} = $self->Id(); + } + elsif ($args{'Target'}) { + $args{'Base'} = $self->Id(); + } + else { + $RT::Logger->debug("$self: Base or Target must be specified\n"); + return (0, 'Either base or target must be specified'); + } + + my $link = new RT::Link($self->CurrentUser); + $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n"); + + $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); + + + + #it's a real link. + if ($link->id) { + $RT::Logger->debug("We're going to delete link ".$link->id."\n"); + $link->Delete(); + + my $TransString= + "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}."; + my ($Trans, $Msg, $TransObj) = $self->_NewTransaction + (Type => 'DeleteLink', + Field => $args{'Type'}, + Data => $TransString, + TimeTaken => 0 + ); + + return ($linkid, "Link deleted ($TransString)", $transactionid); + } + #if it's not a link we can find + else { + $RT::Logger->debug("Couldn't find that link\n"); + return (0, "Link not found"); + } +} + +# }}} + +# {{{ sub AddLink + +=head2 AddLink + +Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket. + + +=cut + +sub AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + @_ ); + + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + if ($args{'Base'} and $args{'Target'}) { + $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n"); + return (0, 'Can\'t specifiy both base and target'); + } + elsif ($args{'Base'}) { + $args{'Target'} = $self->Id(); + } + elsif ($args{'Target'}) { + $args{'Base'} = $self->Id(); + } + else { + return (0, 'Either base or target must be specified'); + } + + # {{{ We don't want references to ourself + if ($args{Base} eq $args{Target}) { + return (0, "Can\'t link a ticket to itself"); + } + + # }}} + + # If the base isn't a URI, make it a URI. + # If the target isn't a URI, make it a URI. + + # {{{ Check if the link already exists - we don't want duplicates + my $old_link= new RT::Link ($self->CurrentUser); + $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); + if ($old_link->Id) { + $RT::Logger->debug("$self Somebody tried to duplicate a link"); + return ($old_link->id, "Link already exists",0); + } + # }}} + + # Storing the link in the DB. + my $link = RT::Link->new($self->CurrentUser); + my ($linkid) = $link->Create(Target => $args{Target}, + Base => $args{Base}, + Type => $args{Type}); + + unless ($linkid) { + return (0,"Link could not be created"); + } + #Write the transaction + + my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}."; + + my ($Trans, $Msg, $TransObj) = $self->_NewTransaction + (Type => 'AddLink', + Field => $args{'Type'}, + Data => $TransString, + TimeTaken => 0 + ); + + return ($Trans, "Link created ($TransString)"); + + +} +# }}} + +# {{{ sub URI + +=head2 URI + +Returns this ticket's URI + +=cut + +sub URI { + my $self = shift; + return $RT::TicketBaseURI.$self->id; +} + +# }}} + +# {{{ sub MergeInto + +=head2 MergeInto +MergeInto take the id of the ticket to merge this ticket into. + +=cut + +sub MergeInto { + my $self = shift; + my $MergeInto = shift; + + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + # Load up the new ticket. + my $NewTicket = RT::Ticket->new($RT::SystemUser); + $NewTicket->Load($MergeInto); + + # make sure it exists. + unless (defined $NewTicket->Id) { + return (0, 'New ticket doesn\'t exist'); + } + + + # Make sure the current user can modify the new ticket. + unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) { + $RT::Logger->debug("failed..."); + return (0, "Permission Denied"); + } + + $RT::Logger->debug("checking if the new ticket has the same id and effective id..."); + unless ($NewTicket->id == $NewTicket->EffectiveId) { + $RT::Logger->err('$self trying to merge into '.$NewTicket->Id . + ' which is itself merged.\n'); + return (0, "Can't merge into a merged ticket. ". + "You should never get this error"); + } + + + # 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 seperate 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 => $NewTicket->Id()); + + unless ($id_val) { + $RT::Logger->error("Couldn't set effective ID for ".$self->Id. + ": $id_msg"); + return(0,"Merge failed. Couldn't set EffectiveId"); + } + + my ($status_val, $status_msg) = $self->__Set(Field => 'Status', + Value => 'resolved'); + + unless ($status_val) { + $RT::Logger->error("$self couldn't set status to resolved.". + "RT's Database may be inconsistent."); + } + + #make a new link: this ticket is merged into that other ticket. + $self->AddLink( Type =>'MergedInto', + Target => $NewTicket->Id() ); + + #add all of this ticket's watchers to that ticket. + my $watchers = $self->Watchers(); + + while (my $watcher = $watchers->Next()) { + unless ( + ($watcher->Owner && + $NewTicket->IsWatcher (Type => $watcher->Type, + Id => $watcher->Owner)) or + ($watcher->Email && + $NewTicket->IsWatcher (Type => $watcher->Type, + Id => $watcher->Email)) + ) { + + + + $NewTicket->_AddWatcher(Silent => 1, + Type => $watcher->Type, + Email => $watcher->Email, + Owner => $watcher->Owner); + } + } + + + #find all of the tickets that were merged into this ticket. + my $old_mergees = new RT::Tickets($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 => $NewTicket->Id()); + } + $NewTicket->_SetLastUpdated; + + return ($TransactionObj, "Merge Successful"); +} + +# }}} + +# }}} + +# {{{ Routines dealing with keywords + +# {{{ sub KeywordsObj + +=head2 KeywordsObj [KEYWORD_SELECT_ID] + + Returns an B object preloaded with this ticket's ObjectKeywords. +If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword +select. + +=cut + +sub KeywordsObj { + my $self = shift; + my $keyword_select; + + $keyword_select = shift if (@_); + + use RT::ObjectKeywords; + my $Keywords = new RT::ObjectKeywords($self->CurrentUser); + + #ACL check + if ($self->CurrentUserHasRight('ShowTicket')) { + $Keywords->LimitToTicket($self->id); + if ($keyword_select) { + $Keywords->LimitToKeywordSelect($keyword_select); + } + } + return ($Keywords); +} +# }}} + +# {{{ sub AddKeyword + +=head2 AddKeyword + +Takes a paramhash of Keyword and KeywordSelect. If Keyword is a valid choice +for KeywordSelect, creates a KeywordObject. If the KeywordSelect says this should +be a single KeywordObject, automatically removes the old value. + + Issues: probably doesn't enforce the depth restrictions or make sure that keywords +are coming from the right part of the tree. really should. + +=cut + +sub AddKeyword { + my $self = shift; + #ACL check + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, 'Permission Denied'); + } + + return($self->_AddKeyword(@_)); + +} + + +# Helper version of AddKeyword without that pesky ACL check +sub _AddKeyword { + my $self = shift; + my %args = ( KeywordSelect => undef, # id of a keyword select record + Keyword => undef, #id of the keyword to add + Silent => 0, + @_ + ); + + my ($OldValue); + + #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'} + + #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue. + + my $Keyword = new RT::Keyword($self->CurrentUser); + unless ($Keyword->Load($args{'Keyword'}) ) { + $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n"); + return(0, "Couldn't load keyword"); + } + + my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser); + unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) { + $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'}); + return(0, "Couldn't load keywordselect"); + } + + my $Keywords = $self->KeywordsObj($KeywordSelectObj->id); + + #If the ticket already has this keyword, just get out of here. + if ($Keywords->HasEntry($Keyword->id)) { + return(0, "That is already the current value"); + } + + #If the keywordselect wants this to be a singleton: + + if ($KeywordSelectObj->Single) { + + #Whack any old values...keep track of the last value that we get. + #we shouldn't need a loop ehre, but we do it anyway, to try to + # help keep the database clean. + while (my $OldKey = $Keywords->Next) { + $OldValue = $OldKey->KeywordObj->Name; + $OldKey->Delete(); + } + + + } + + # create the new objectkeyword + my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); + my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id, + ObjectType => 'Ticket', + ObjectId => $self->Id, + KeywordSelect => $KeywordSelectObj->Id ); + + + # record a single transaction, unless we were told not to + unless ($args{'Silent'}) { + my ($TransactionId, $Msg, $TransactionObj) = + $self->_NewTransaction( Type => 'Keyword', + Field => $KeywordSelectObj->Id, + OldValue => $OldValue, + NewValue => $Keyword->Name ); + } + return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added."); + +} + +# }}} + +# {{{ sub DeleteKeyword + +=head2 DeleteKeyword + + Takes a paramhash. Deletes the Keyword denoted by the I parameter from this + ticket's object keywords. + +=cut + +sub DeleteKeyword { + my $self = shift; + my %args = ( Keyword => undef, + KeywordSelect => undef, + @_ ); + + #ACL check + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, 'Permission Denied'); + } + + + #Load up the ObjectKeyword we\'re talking about + my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); + $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'}, + KeywordSelect => $args{'KeywordSelect'}, + ObjectType => 'Ticket', + ObjectId => $self->id() + ); + + #if we can\'t find it, bail + unless ($ObjectKeyword->id) { + $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} . + " for keywordselect ". $args{'KeywordSelect'} . + "for ticket ".$self->id ); + return (undef, "Couldn't load keyword while trying to delete it."); + }; + + #record transaction here. + my ($TransactionId, $Msg, $TransObj) = + $self->_NewTransaction( Type => 'Keyword', + OldValue => $ObjectKeyword->KeywordObj->Name); + + $ObjectKeyword->Delete(); + + return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted."); + +} + +# }}} + +# }}} + +# {{{ Routines dealing with ownership + +# {{{ sub OwnerObj + +=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 + + $owner = new RT::User ($self->CurrentUser); + $owner->Load($self->__Value('Owner')); + + #Return the owner object + return ($owner); +} + +# }}} + +# {{{ sub OwnerAsString + +=head2 OwnerAsString + +Returns the owner's email address + +=cut + +sub OwnerAsString { + my $self = shift; + return($self->OwnerObj->EmailAddress); + +} + +# }}} + +# {{{ sub SetOwner + +=head2 SetOwner + +Takes two arguments: + the Id or Name of the owner +and (optionally) the type of the SetOwner Transaction. It defaults +to 'Give'. 'Steal' is also a valid option. + +=cut + +sub SetOwner { + my $self = shift; + my $NewOwner = shift; + my $Type = shift || "Give"; + + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + my $NewOwnerObj = RT::User->new($self->CurrentUser); + my $OldOwnerObj = $self->OwnerObj; + + $NewOwnerObj->Load($NewOwner); + if (!$NewOwnerObj->Id) { + return (0, "That user does not exist"); + } + + #If thie ticket has an owner and it's not the current user + + if (($Type ne 'Steal' ) and ($Type ne 'Force') and #If we're not stealing + ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set + ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us + return(0, "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->Id) and + (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket', + QueueObj => $self->QueueObj, + TicketObj => $self)) + ) { + return (0, "That user may not own requests in that queue"); + } + + + #If the ticket has an owner and it's the new owner, we don't need + #To do anything + elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) { + return(0, "That user already owns that request"); + } + + + my ($trans,$msg)=$self->_Set(Field => 'Owner', + Value => $NewOwnerObj->Id, + TimeTaken => 0, + TransactionType => $Type); + + if ($trans) { + $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name; + } + return ($trans, $msg); + +} + +# }}} + +# {{{ sub Take + +=head2 Take + +A convenince method to set the ticket's owner to the current user + +=cut + +sub Take { + my $self = shift; + return ($self->SetOwner($self->CurrentUser->Id, 'Take')); +} + +# }}} + +# {{{ sub Untake + +=head2 Untake + +Convenience method to set the owner to 'nobody' if the current user is the owner. + +=cut + +sub Untake { + my $self = shift; + return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake')); +} +# }}} + +# {{{ sub Steal + +=head2 Steal + +A convenience method to change the owner of the current ticket to the +current user. Even if it's owned by another user. + +=cut + +sub Steal { + my $self = shift; + + if ($self->IsOwner($self->CurrentUser)) { + return (0,"You already own this ticket"); + } else { + return($self->SetOwner($self->CurrentUser->Id, 'Steal')); + + } + +} + +# }}} + +# }}} + +# {{{ Routines dealing with status + +# {{{ sub ValidateStatus + +=head2 ValidateStatus STATUS + +Takes a string. Returns true if that status is a valid status for this ticket. +Returns false otherwise. + +=cut + +sub ValidateStatus { + my $self = shift; + my $status = shift; + + #Make sure the status passed in is valid + unless ($self->QueueObj->IsValidStatus($status)) { + return (undef); + } + + return (1); + +} + + +# }}} + +# {{{ sub SetStatus + +=head2 SetStatus STATUS + +Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead. + +=cut + +sub SetStatus { + my $self = shift; + my $status = shift; + + #Check ACL + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, 'Permission Denied'); + } + + my $now = new RT::Date($self->CurrentUser); + $now->SetToNow(); + + #If we're changing the status from new, record that we've started + if (($self->Status =~ /new/) && ($status ne 'new')) { + #Set the Started time to "now" + $self->_Set(Field => 'Started', + Value => $now->ISO, + RecordTransaction => 0); + } + + + if ($status eq 'resolved') { + #When we resolve a ticket, set the 'Resolved' attribute to now. + $self->_Set(Field => 'Resolved', + Value => $now->ISO, + RecordTransaction => 0); + } + + + #Actually update the status + return($self->_Set(Field => 'Status', + Value => $status, + TimeTaken => 0, + TransactionType => 'Status')); +} + +# }}} + +# {{{ sub Kill + +=head2 Kill + +Takes no arguments. Marks this ticket for garbage collection + +=cut + +sub Kill { + my $self = shift; + return ($self->SetStatus('dead')); + # TODO: garbage collection +} + +# }}} + +# {{{ sub Stall + +=head2 Stall + +Sets this ticket's status to stalled + +=cut + +sub Stall { + my $self = shift; + return ($self->SetStatus('stalled')); +} + +# }}} + +# {{{ sub Open + +=head2 Open + +Sets this ticket\'s status to Open + +=cut + +sub Open { + my $self = shift; + return ($self->SetStatus('open')); +} + +# }}} + +# {{{ sub Resolve + +=head2 Resolve + +Sets this ticket\'s status to Resolved + +=cut + +sub Resolve { + my $self = shift; + return ($self->SetStatus('resolved')); +} + +# }}} + +# }}} + +# {{{ Actions + Routines dealing with transactions + +# {{{ sub SetTold and _SetTold + +=head2 SetTold ISO [TIMETAKEN] + +Updates the told and records a transaction + +=cut + +sub SetTold { + my $self=shift; + my $told; + $told = shift if (@_); + my $timetaken=shift || 0; + + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + my $datetold = new RT::Date($self->CurrentUser); + if ($told) { + $datetold->Set( Format => 'iso', + Value => $told); + } + else { + $datetold->SetToNow(); + } + + return($self->_Set(Field => 'Told', + Value => $datetold->ISO, + TimeTaken => $timetaken, + TransactionType => 'Told')); +} + +=head2 _SetTold + +Updates the told without a transaction or acl check. Useful when we're sending replies. + +=cut + +sub _SetTold { + my $self=shift; + + my $now = new RT::Date($self->CurrentUser); + $now->SetToNow(); + #use __Set to get no ACLs ;) + return($self->__Set(Field => 'Told', + Value => $now->ISO)); +} + +# }}} + +# {{{ sub Transactions + +=head2 Transactions + + Returns an RT::Transactions object of all transactions on this ticket + +=cut + +sub Transactions { + my $self = shift; + + use RT::Transactions; + my $transactions = RT::Transactions->new($self->CurrentUser); + + #If the user has no rights, return an empty object + if ($self->CurrentUserHasRight('ShowTicket')) { + my $tickets = $transactions->NewAlias('Tickets'); + $transactions->Join( ALIAS1 => 'main', + FIELD1 => 'Ticket', + ALIAS2 => $tickets, + FIELD2 => 'id'); + $transactions->Limit( ALIAS => $tickets, + FIELD => 'EffectiveId', + VALUE => $self->id()); + # if the user may not see comments do not return them + unless ($self->CurrentUserHasRight('ShowTicketComments')) { + $transactions->Limit( FIELD => 'Type', + OPERATOR => '!=', + VALUE => "Comment"); + } + } + + return($transactions); +} + +# }}} + +# {{{ sub _NewTransaction + +sub _NewTransaction { + my $self = shift; + my %args = ( TimeTaken => 0, + Type => undef, + OldValue => undef, + NewValue => undef, + Data => undef, + Field => undef, + MIMEObj => undef, + @_ ); + + + require RT::Transaction; + my $trans = new RT::Transaction($self->CurrentUser); + my ($transaction, $msg) = + $trans->Create( Ticket => $self->Id, + TimeTaken => $args{'TimeTaken'}, + Type => $args{'Type'}, + Data => $args{'Data'}, + Field => $args{'Field'}, + NewValue => $args{'NewValue'}, + OldValue => $args{'OldValue'}, + MIMEObj => $args{'MIMEObj'} + ); + + $RT::Logger->warning($msg) unless $transaction; + + $self->_SetLastUpdated; + + if (defined $args{'TimeTaken'} ) { + $self->_UpdateTimeTaken($args{'TimeTaken'}); + } + return($transaction, $msg, $trans); +} + +# }}} + +# }}} + +# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record + +# {{{ sub _ClassAccessible + +sub _ClassAccessible { + { + EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 }, + Queue => { 'read' => 1, 'write' => 1 }, + Requestors => { 'read' => 1, 'write' => 1 }, + Owner => { 'read' => 1, 'write' => 1 }, + Subject => { 'read' => 1, 'write' => 1 }, + InitialPriority => { 'read' => 1, 'write' => 1 }, + FinalPriority => { 'read' => 1, 'write' => 1 }, + Priority => { 'read' => 1, 'write' => 1 }, + Status => { 'read' => 1, 'write' => 1 }, + TimeWorked => { 'read' => 1, 'write' => 1 }, + TimeLeft => { 'read' => 1, 'write' => 1 }, + Created => { 'read' => 1, 'auto' => 1 }, + Creator => { 'read' => 1, 'auto' => 1 }, + Told => { 'read' => 1, 'write' => 1 }, + Resolved => {'read' => 1}, + Starts => { 'read' => 1, 'write' => 1 }, + Started => { 'read' => 1, 'write' => 1 }, + Due => { 'read' => 1, 'write' => 1 }, + Creator => { 'read' => 1, 'auto' => 1 }, + Created => { 'read' => 1, 'auto' => 1 }, + LastUpdatedBy => { 'read' => 1, 'auto' => 1 }, + LastUpdated => { 'read' => 1, 'auto' => 1 } + }; + +} + +# }}} + +# {{{ sub _Set + +sub _Set { + my $self = shift; + + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return (0, "Permission Denied"); + } + + my %args = (Field => undef, + Value => undef, + TimeTaken => 0, + RecordTransaction => 1, + TransactionType => 'Set', + @_ + ); + #if the user is trying to modify the record + + #Take care of the old value we really don't want to get in an ACL loop. + # so ask the super::_Value + my $Old=$self->SUPER::_Value("$args{'Field'}"); + + #Set the new value + my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, + Value=> $args{'Value'}); + + #If we can't actually set the field to the value, don't record + # a transaction. instead, get out of here. + if ($ret==0) {return (0,$msg);} + + if ($args{'RecordTransaction'} == 1) { + + my ($Trans, $Msg, $TransObj) = + $self->_NewTransaction(Type => $args{'TransactionType'}, + Field => $args{'Field'}, + NewValue => $args{'Value'}, + OldValue => $Old, + TimeTaken => $args{'TimeTaken'}, + ); + return ($Trans,$TransObj->Description); + } + else { + return ($ret, $msg); + } +} + +# }}} + +# {{{ sub _Value + +=head2 _Value + +Takes the name of a table column. +Returns its value as a string, if the user passes an ACL check + +=cut + +sub _Value { + + my $self = shift; + my $field = shift; + + + #if the field is public, return it. + if ($self->_Accessible($field, 'public')) { + #$RT::Logger->debug("Skipping ACL check for $field\n"); + return($self->SUPER::_Value($field)); + + } + + #If the current user doesn't have ACLs, don't let em at it. + + unless ($self->CurrentUserHasRight('ShowTicket')) { + return (undef); + } + return($self->SUPER::_Value($field)); + +} + +# }}} + +# {{{ sub _UpdateTimeTaken + +=head2 _UpdateTimeTaken + +This routine will increment the timeworked counter. it should +only be called from _NewTransaction + +=cut + +sub _UpdateTimeTaken { + my $self = shift; + my $Minutes = shift; + my ($Total); + + $Total = $self->SUPER::_Value("TimeWorked"); + $Total = ($Total || 0) + ($Minutes || 0); + $self->SUPER::_Set(Field => "TimeWorked", + Value => $Total); + + return ($Total); +} + +# }}} + +# }}} + +# {{{ Routines dealing with ACCESS CONTROL + +# {{{ sub CurrentUserHasRight + +=head2 CurrentUserHasRight + + Takes the textual name of a Ticket scoped right (from RT::ACE) and returns +1 if the user has that right. It returns 0 if the user doesn't have that right. + +=cut + +sub CurrentUserHasRight { + my $self = shift; + my $right = shift; + + return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(), + Right => "$right")); + +} + +# }}} + +# {{{ sub HasRight + +=head2 HasRight + + Takes a paramhash with the attributes 'Right' and 'Principal' + 'Right' is a ticket-scoped textual right from RT::ACE + 'Principal' is an RT::User object + + Returns 1 if the principal has the right. Returns undef if not. + +=cut + +sub HasRight { + my $self = shift; + my %args = ( Right => undef, + Principal => undef, + @_); + + unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) { + $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight"); + } + + return($args{'Principal'}->HasQueueRight(TicketObj => $self, + Right => $args{'Right'})); +} + +# }}} + +# }}} + + +1; + +=head1 AUTHOR + +Jesse Vincent, jesse@fsck.com + +=head1 SEE ALSO + +RT + +=cut + + -- cgit v1.2.1 From ded0451e9582df33cae6099a2fb72b4ea25076cf Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 15 Jul 2003 13:30:43 +0000 Subject: reverting to vendor branch rt 3.0.4, hopefully --- rt/lib/RT/Ticket.pm | 3026 ++++++--------------------------------------------- 1 file changed, 342 insertions(+), 2684 deletions(-) (limited to 'rt/lib/RT/Ticket.pm') diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm index f7275e4e3..2f075a20c 100755 --- a/rt/lib/RT/Ticket.pm +++ b/rt/lib/RT/Ticket.pm @@ -1,3004 +1,662 @@ -# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ -# (c) 1996-2001 Jesse Vincent -# This software is redistributable under the terms of the GNU GPL +# BEGIN LICENSE BLOCK +# +# Copyright (c) 1996-2003 Jesse Vincent +# +# (Except where explictly superceded by other copyright notices) +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# Unless otherwise specified, all modifications, corrections or +# extensions to this work which alter its source code become the +# property of Best Practical Solutions, LLC when submitted for +# inclusion in the work. +# +# +# END LICENSE BLOCK +# Autogenerated by DBIx::SearchBuilder factory (by ) +# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST. +# +# !! DO NOT EDIT THIS FILE !! # -=head1 NAME +use strict; - RT::Ticket - RT ticket object -=head1 SYNOPSIS +=head1 NAME - use RT::Ticket; - my $ticket = new RT::Ticket($CurrentUser); - $ticket->Load($ticket_id); +RT::Ticket -=head1 DESCRIPTION -This module lets you manipulate RT\'s ticket object. +=head1 SYNOPSIS +=head1 DESCRIPTION =head1 METHODS =cut - - package RT::Ticket; +use RT::Record; use RT::Queue; -use RT::User; -use RT::Record; -use RT::Link; -use RT::Links; -use RT::Date; -use RT::Watcher; - - -@ISA= qw(RT::Record); - -=begin testing -use RT::TestHarness; - -ok(require RT::Ticket, "Loading the RT::Ticket library"); - -=end testing - -=cut - -# {{{ sub _Init +use vars qw( @ISA ); +@ISA= qw( RT::Record ); sub _Init { - my $self = shift; - $self->{'table'} = "Tickets"; - return ($self->SUPER::_Init(@_)); -} - -# }}} - -# {{{ sub Load - -=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; - - #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. - - #If it's a local URI, turn it into a ticket id - if ($id =~ /^$RT::TicketBaseURI(\d+)$/) { - $id = $1; - } - #If it's a remote URI, we're going to punt for now - elsif ($id =~ '://' ) { - return (undef); - } - - #If we have an integer URI, load the ticket - if ( $id =~ /^\d+$/ ) { - my $ticketid = $self->LoadById($id); - - unless ($ticketid) { - $RT::Logger->debug("$self tried to load a bogus ticket: $id\n"); - return(undef); - } - } - - #It's not a URI. It's not a numerical ticket ID. Punt! - else { - return(undef); - } - - #If we're merged, resolve the merge. - if (($self->EffectiveId) and - ($self->EffectiveId != $self->Id)) { - return ($self->Load($self->EffectiveId)); - } - - #Ok. we're loaded. lets get outa here. - return ($self->Id); - -} - -# }}} + my $self = shift; -# {{{ sub LoadByURI - -=head2 LoadByURI - -Given a local ticket URI, loads the specified ticket. - -=cut - -sub LoadByURI { - my $self = shift; - my $uri = shift; - - if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) { - my $id = $1; - return ($self->Load($id)); - } - else { - return(undef); - } + $self->Table('Tickets'); + $self->SUPER::_Init(@_); } -# }}} - -# {{{ sub Create - -=head2 Create (ARGS) - -Arguments: ARGS is a hash of named parameters. Valid parameters are: - - Queue - Either a Queue object or a Queue Name - Requestor - A reference to a list of RT::User objects, email addresses or RT user Names - Cc - A reference to a list of RT::User objects, email addresses or Names - AdminCc - A reference to a list of RT::User objects, email addresses or Names - 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 - InitialPriority -- an integer from 0 to 99 - FinalPriority -- an integer from 0 to 99 - Status -- any valid status (Defined in RT::Queue) - TimeWorked -- an integer - TimeLeft -- an integer - 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. - - KeywordSelect- -- an array of keyword ids for that keyword select - - -Returns: TICKETID, Transaction Object, Error Message - - -=begin testing - -my $t = RT::Ticket->new($RT::SystemUser); - -ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created"); - -ok ( my $id = $t->Id, "Got ticket id"); - -=end testing - -=cut - -sub Create { - my $self = shift; - - my %args = ( - Queue => undef, - Requestor => undef, - Cc => undef, - AdminCc => undef, - Type => 'ticket', - Owner => $RT::Nobody->UserObj, - Subject => '[no subject]', - InitialPriority => undef, - FinalPriority => undef, - Status => 'new', - TimeWorked => "0", - TimeLeft => 0, - Due => undef, - Starts => undef, - MIMEObj => undef, - @_); - - my ($ErrStr, $QueueObj, $Owner, $resolved); - my (@non_fatal_errors); - - my $now = RT::Date->new($self->CurrentUser); - $now->SetToNow(); - - if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) { - $QueueObj=RT::Queue->new($RT::SystemUser); - $QueueObj->Load($args{'Queue'}); - } - 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)) { - $RT::Logger->debug( "$self No queue given for ticket creation."); - return (0, 0,'Could not create ticket. Queue not set'); - } - - #Now that we have a queue, Check the ACLS - unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', - QueueObj => $QueueObj )) { - return (0,0,"No permission to create tickets in the queue '". - $QueueObj->Name."'."); - } - - #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'}); - - - #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 = new RT::Date($self->CurrentUser); - if (defined $args{'Due'}) { - $due->Set (Format => 'ISO', - Value => $args{'Due'}); - } - elsif (defined ($QueueObj->DefaultDueIn)) { - $due->SetToNow; - $due->AddDays($QueueObj->DefaultDueIn); - } - - my $starts = new RT::Date($self->CurrentUser); - if (defined $args{'Starts'}) { - $starts->Set (Format => 'ISO', - Value => $args{'Starts'}); - } - - - # {{{ Deal with setting the owner - - if (ref($args{'Owner'}) eq 'RT::User') { - $Owner = $args{'Owner'}; - } - #If we've been handed something else, try to load the user. - elsif ($args{'Owner'}) { - $Owner = new RT::User($self->CurrentUser); - $Owner->Load($args{'Owner'}); - - } - #If we can't handle it, call it nobody - else { - if (ref($args{'Owner'})) { - $RT::Logger->warning("$ticket ->Create called with an Owner of ". - "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n"); - - push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; - } - else { - $RT::Logger->warning("$self ->Create called with an ". - "unknown datatype for Owner: ".$args{'Owner'} . - ". Defaulting to Nobody.\n"); - } - } - - #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->HasQueueRight( QueueObj => $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 this queue\n"); - - push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; - - $Owner = undef; - } - - #If we haven't been handed a valid owner, make it nobody. - unless (defined ($Owner)) { - $Owner = new RT::User($self->CurrentUser); - $Owner->Load($RT::Nobody->UserObj->Id); - } - - # }}} - - unless ($self->ValidateStatus($args{'Status'})) { - return (0,0,'Invalid value for status'); - } - - if ($args{'Status'} eq 'resolved') { - $resolved = $now->ISO; - } else{ - $resolved = undef; - } - - my $id = $self->SUPER::Create( - Queue => $QueueObj->Id, - Owner => $Owner->Id, - Subject => $args{'Subject'}, - InitialPriority => $args{'InitialPriority'}, - FinalPriority => $args{'FinalPriority'}, - Priority => $args{'InitialPriority'}, - Status => $args{'Status'}, - TimeWorked => $args{'TimeWorked'}, - TimeLeft => $args{'TimeLeft'}, - Type => $args{'Type'}, - Starts => $starts->ISO, - Resolved => $resolved, - Due => $due->ISO - ); - #Set the ticket's effective ID now that we've created it. - my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id); - - unless ($val) { - $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n"); - } - - - my $watcher; - foreach $watcher (@{$args{'Cc'}}) { - my ($wval, $wmsg) = - $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); - push @non_fatal_errors, $wmsg unless ($wval); - } - - foreach $watcher (@{$args{'Requestor'}}) { - my ($wval, $wmsg) = - $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); - push @non_fatal_errors, $wmsg unless ($wval); - } - - foreach $watcher (@{$args{'AdminCc'}}) { - # 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 ($wval, $wmsg) = - $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); - push @non_fatal_errors, $wmsg unless ($wval); - } - - # Iterate through all the KeywordSelect- params passed in, calling _AddKeyword - # for each of them - - - foreach my $key (keys %args) { - - next unless ($key =~ /^KeywordSelect-(.*)$/); - - my $ks = $1; - - - my @keywords = ref($args{$key}) eq 'ARRAY' ? - @{$args{$key}} : ($args{$key}); - - foreach my $keyword (@keywords) { - my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks, - Keyword => $keyword, - Silent => 1); - } - push @non_fatal_errors, $kmsg unless ($kval); - } - - - - #Add a transaction for the create - my ($Trans, $Msg, $TransObj) = - $self->_NewTransaction( Type => "Create", - TimeTaken => 0, - MIMEObj=>$args{'MIMEObj'}); - - # Logging - if ($self->Id && $Trans) { - $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name. - "'.\n" . join("\n", @non_fatal_errors); - - $RT::Logger->info($ErrStr); - } - else { - # TODO where does this get errstr from? - $RT::Logger->warning("Ticket couldn't be created: $ErrStr"); - } - - return($self->Id, $TransObj->Id, $ErrStr); -} -# }}} -# {{{ sub Import -=head2 Import PARAMHASH -Import a ticket. -Doesn\'t create a transaction. -Doesn\'t supply queue defaults, etc. +=item Create PARAMHASH -Arguments are identical to Create(), with the addition of - Id - Ticket Id +Create takes a hash of values and creates a row in the database: -Returns: TICKETID + int(11) 'EffectiveId'. + int(11) 'Queue'. + varchar(16) 'Type'. + int(11) 'IssueStatement'. + int(11) 'Resolution'. + int(11) 'Owner'. + varchar(200) 'Subject' defaults to '[no subject]'. + int(11) 'InitialPriority'. + int(11) 'FinalPriority'. + int(11) 'Priority'. + int(11) 'TimeEstimated'. + int(11) 'TimeWorked'. + varchar(10) 'Status'. + int(11) 'TimeLeft'. + datetime 'Told'. + datetime 'Starts'. + datetime 'Started'. + datetime 'Due'. + datetime 'Resolved'. + smallint(6) 'Disabled'. =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,'Could not create ticket. Queue not set'); - } - - #Now that we have a queue, Check the ACLS - unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', - QueueObj => $QueueObj )) { - return (0,"No permission to create tickets in the queue '". - $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 = new RT::User($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->HasQueueRight( QueueObj => $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."'\n"); - - $Owner = undef; - } - - #If we haven't been handed a valid owner, make it nobody. - unless (defined ($Owner)) { - $Owner = new RT::User($self->CurrentUser); - $Owner->Load($RT::Nobody->UserObj->Id); - } - - # }}} - - unless ($self->ValidateStatus($args{'Status'})) { - return (0,"'$args{'Status'}' is an invalid value for 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'}, - InitialPriority => $args{'InitialPriority'}, - FinalPriority => $args{'FinalPriority'}, - Priority => $args{'InitialPriority'}, - Status => $args{'Status'}, - TimeWorked => $args{'TimeWorked'}, - Type => $args{'Type'}, - Created => $args{'Created'}, - Told => $args{'Told'}, - LastUpdated => $args{'Updated'}, - Resolved => $args{Resolved}, - Due => $args{'Due'}, - ); - - - - # 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\n"); - } - } - - my $watcher; - foreach $watcher (@{$args{'Cc'}}) { - $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); - } - foreach $watcher (@{$args{'AdminCc'}}) { - $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); - } - foreach $watcher (@{$args{'Requestor'}}) { - $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); - } - - return($self->Id, $ErrStr); -} - -# }}} - -# {{{ sub Delete - -sub Delete { - my $self = shift; - return (0, 'Deleting this object would violate referential integrity.'. - ' That\'s bad.'); -} -# }}} - -# {{{ Routines dealing with watchers. - -# {{{ Routines dealing with adding new watchers - -# {{{ sub AddWatcher - -=head2 AddWatcher - -AddWatcher takes a parameter hash. The keys are as follows: - -Email -Type -Owner - -If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address. - -=cut - -sub AddWatcher { - my $self = shift; - my %args = ( Email => undef, - Type => undef, - Owner => undef, - @_ - ); - - # {{{ Check ACLS - #If the watcher we're trying to add is for the current user - if ( ( $self->CurrentUser->EmailAddress && - ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or - ($args{'Owner'} eq $self->CurrentUser->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, '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, 'Permission Denied'); - } - } - else { - $RT::Logger->warn("$self -> AddWatcher hit code". - " it never should. We got passed ". - " a type of ". $args{'Type'}); - return (0,'Error in parameters to TicketAddWatcher'); - } - } - # If the watcher isn't the current user - # and the current user doesn't have 'ModifyTicket' - # bail - else { - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - } - # }}} - - 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, - Email => undef, - Owner => 0, - Person => undef, - @_ ); - - - - #clear the watchers cache - $self->{'watchers_cache'} = undef; - - if (defined $args{'Person'}) { - #if it's an RT::User object, pull out the id and shove it in Owner - if (ref ($args{'Person'}) =~ /RT::User/) { - $args{'Owner'} = $args{'Person'}->id; - } - #if it's an int, shove it in Owner - elsif ($args{'Person'} =~ /^\d+$/) { - $args{'Owner'} = $args{'Person'}; - } - #if it's an email address, shove it in Email - else { - $args{'Email'} = $args{'Person'}; - } - } - - # Turn an email address int a watcher if we possibly can. - if ($args{'Email'}) { - my $watcher = new RT::User($self->CurrentUser); - $watcher->LoadByEmail($args{'Email'}); - if ($watcher->Id) { - $args{'Owner'} = $watcher->Id; - delete $args{'Email'}; - } - } - - - # see if this user is already a watcher. if we have an owner, check it - # otherwise, we've got an email-address watcher. use that. - - if ($self->IsWatcher(Type => $args{'Type'}, - Id => ($args{'Owner'} || $args{'Email'}) ) ) { - - - return(0, 'That user is already that sort of watcher for this ticket'); - } - - - require RT::Watcher; - my $Watcher = new RT::Watcher ($self->CurrentUser); - my ($retval, $msg) = ($Watcher->Create( Value => $self->Id, - Scope => 'Ticket', - Email => $args{'Email'}, - Type => $args{'Type'}, - Owner => $args{'Owner'}, - )); - - unless ($args{'Silent'}) { - $self->_NewTransaction( Type => 'AddWatcher', - NewValue => $Watcher->Email, - Field => $Watcher->Type); - } - - return ($retval, $msg); -} - -# }}} - -# {{{ sub AddRequestor - -=head2 AddRequestor - -AddRequestor takes what AddWatcher does, except it presets -the "Type" parameter to \'Requestor\' - -=cut - -sub AddRequestor { - my $self = shift; - return ($self->AddWatcher ( Type => 'Requestor', @_)); -} - -# }}} - -# {{{ sub AddCc - -=head2 AddCc - -AddCc takes what AddWatcher does, except it presets -the "Type" parameter to \'Cc\' - -=cut - -sub AddCc { - my $self = shift; - return ($self->AddWatcher ( Type => 'Cc', @_)); -} -# }}} - -# {{{ sub AddAdminCc - -=head2 AddAdminCc - -AddAdminCc takes what AddWatcher does, except it presets -the "Type" parameter to \'AdminCc\' - -=cut - -sub AddAdminCc { - my $self = shift; - return ($self->AddWatcher ( Type => 'AdminCc', @_)); -} - -# }}} - -# }}} - -# {{{ sub DeleteWatcher - -=head2 DeleteWatcher id [type] - -DeleteWatcher takes a single argument which is either an email address -or a watcher id. -If the first argument is an email address, you need to specify the watcher type you're talking -about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'. -It removes that watcher from this Ticket\'s list of watchers. - - -=cut -#TODO It is lame that you can't call this the same way you can call AddWatcher -sub DeleteWatcher { +sub Create { my $self = shift; - my $id = shift; - - my $type; - - $type = shift if (@_); - - my $Watcher = new RT::Watcher($self->CurrentUser); - - #If it\'s a numeric watcherid - if ($id =~ /^(\d*)$/) { - $Watcher->Load($id); - } - - #Otherwise, we'll assume it's an email address - elsif ($type) { - my ($result, $msg) = - $Watcher->LoadByValue( Email => $id, - Scope => 'Ticket', - Value => $self->id, - Type => $type); - return (0,$msg) unless ($result); - } - - else { - return(0,"Can\'t delete a watcher by email address without specifying a type"); - } - - # {{{ Check ACLS - - #If the watcher we're trying to delete is for the current user - if ($Watcher->Email eq $self->CurrentUser->EmailAddress) { - - # If it's an AdminCc and they don't have - # 'WatchAsAdminCc' or 'ModifyTicket', bail - if ($Watcher->Type eq 'AdminCc') { - unless ($self->CurrentUserHasRight('ModifyTicket') or - $self->CurrentUserHasRight('WatchAsAdminCc')) { - return(0, 'Permission Denied'); - } - } - - # If it's a Requestor or Cc and they don't have - # 'Watch' or 'ModifyTicket', bail - elsif (($Watcher->Type eq 'Cc') or - ($Watcher->Type eq 'Requestor')) { - - unless ($self->CurrentUserHasRight('ModifyTicket') or - $self->CurrentUserHasRight('Watch')) { - return(0, 'Permission Denied'); - } - } - else { - $RT::Logger->warn("$self -> DeleteWatcher hit code". - " it never should. We got passed ". - " a type of ". $args{'Type'}); - return (0,'Error in parameters to $self 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, "Permission Denied"); - } - } - - # }}} - - unless (($Watcher->Scope eq 'Ticket') and - ($Watcher->Value == $self->id) ) { - return (0, "Not a watcher for this ticket"); - } - - - #Clear out the watchers hash. - $self->{'watchers'} = undef; - - #If we\'ve validated that it is a watcher for this ticket - $self->_NewTransaction ( Type => 'DelWatcher', - OldValue => $Watcher->Email, - Field => $Watcher->Type, - ); - - my $retval = $Watcher->Delete(); - - unless ($retval) { - return(0,"Watcher could not be deleted. Database inconsistency possible."); - } - - return(1, "Watcher deleted"); -} - -# {{{ sub DeleteRequestor - -=head2 DeleteRequestor EMAIL + my %args = ( + EffectiveId => '0', + Queue => '0', + Type => '', + IssueStatement => '0', + Resolution => '0', + Owner => '0', + Subject => '[no subject]', + InitialPriority => '0', + FinalPriority => '0', + Priority => '0', + TimeEstimated => '0', + TimeWorked => '0', + Status => '', + TimeLeft => '0', + Told => '', + Starts => '', + Started => '', + Due => '', + Resolved => '', + Disabled => '0', -Takes an email address. It calls DeleteWatcher with a preset -type of 'Requestor' - - -=cut + @_); + $self->SUPER::Create( + EffectiveId => $args{'EffectiveId'}, + Queue => $args{'Queue'}, + Type => $args{'Type'}, + IssueStatement => $args{'IssueStatement'}, + Resolution => $args{'Resolution'}, + Owner => $args{'Owner'}, + Subject => $args{'Subject'}, + InitialPriority => $args{'InitialPriority'}, + FinalPriority => $args{'FinalPriority'}, + Priority => $args{'Priority'}, + TimeEstimated => $args{'TimeEstimated'}, + TimeWorked => $args{'TimeWorked'}, + Status => $args{'Status'}, + TimeLeft => $args{'TimeLeft'}, + Told => $args{'Told'}, + Starts => $args{'Starts'}, + Started => $args{'Started'}, + Due => $args{'Due'}, + Resolved => $args{'Resolved'}, + Disabled => $args{'Disabled'}, +); -sub DeleteRequestor { - my $self = shift; - my $id = shift; - return ($self->DeleteWatcher ($id, 'Requestor')) } -# }}} - -# {{{ sub DeleteCc - -=head2 DeleteCc EMAIL - -Takes an email address. It calls DeleteWatcher with a preset -type of 'Cc' - - -=cut - -sub DeleteCc { - my $self = shift; - my $id = shift; - return ($self->DeleteWatcher ($id, 'Cc')) -} - -# }}} - -# {{{ sub DeleteAdminCc - -=head2 DeleteAdminCc EMAIL - -Takes an email address. It calls DeleteWatcher with a preset -type of 'AdminCc' - - -=cut - -sub DeleteAdminCc { - my $self = shift; - my $id = shift; - return ($self->DeleteWatcher ($id, 'AdminCc')) -} - -# }}} - - -# }}} -# {{{ sub Watchers -=head2 Watchers +=item id -Watchers returns a Watchers object preloaded with this ticket\'s watchers. +Returns the current value of id. +(In the database, id is stored as int(11).) -# It should return only the ticket watchers. the actual FooAsString -# methods capture the queue watchers too. I don't feel thrilled about this, -# but we don't want the Cc Requestors and AdminCc objects to get filled up -# with all the queue watchers too. we've got seperate objects for that. - # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch? =cut -sub Watchers { - my $self = shift; - - require RT::Watchers; - my $watchers=RT::Watchers->new($self->CurrentUser); - if ($self->CurrentUserHasRight('ShowTicket')) { - $watchers->LimitToTicket($self->id); - } - - return($watchers); - -} - -# }}} -# {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string +=item EffectiveId -=head2 RequestorsAsString +Returns the current value of EffectiveId. +(In the database, EffectiveId is stored as int(11).) - B String: All Ticket Requestor email addresses as a string. -=cut -sub RequestorsAsString { - my $self=shift; +=item SetEffectiveId VALUE - unless ($self->CurrentUserHasRight('ShowTicket')) { - return undef; - } - - return ($self->Requestors->EmailsAsString() ); -} -=head2 WatchersAsString +Set EffectiveId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, EffectiveId will be stored as a int(11).) -B String: All Ticket Watchers email addresses as a string =cut -sub WatchersAsString { - my $self=shift; - - unless ($self->CurrentUserHasRight('ShowTicket')) { - return (0, "Permission Denied"); - } - - return ($self->Watchers->EmailsAsString()); - -} -=head2 AdminCcAsString +=item Queue -returns String: All Ticket AdminCc email addresses as a string +Returns the current value of Queue. +(In the database, Queue is stored as int(11).) -=cut -sub AdminCcAsString { - my $self=shift; +=item SetQueue VALUE - unless ($self->CurrentUserHasRight('ShowTicket')) { - return undef; - } - - return ($self->AdminCc->EmailsAsString()); - -} -=head2 CcAsString +Set Queue to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Queue will be stored as a int(11).) -returns String: All Ticket Ccs as a string of email addresses =cut -sub CcAsString { - my $self=shift; - - unless ($self->CurrentUserHasRight('ShowTicket')) { - return undef; - } - - return ($self->Cc->EmailsAsString()); - -} - -# }}} - -# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs -# {{{ sub Requestors +=item QueueObj -=head2 Requestors +Returns the Queue Object which has the id returned by Queue -Takes nothing. -Returns this ticket's Requestors as an RT::Watchers object - -=cut - -sub Requestors { - my $self = shift; - - my $requestors = $self->Watchers(); - if ($self->CurrentUserHasRight('ShowTicket')) { - $requestors->LimitToRequestors(); - } - - return($requestors); - -} - -# }}} - -# {{{ sub Cc - -=head2 Cc - -Takes nothing. -Returns a watchers object which contains this ticket's Cc watchers - -=cut - -sub Cc { - my $self = shift; - - my $cc = $self->Watchers(); - - if ($self->CurrentUserHasRight('ShowTicket')) { - $cc->LimitToCc(); - } - - return($cc); - -} - -# }}} - -# {{{ sub AdminCc - -=head2 AdminCc - -Takes nothing. -Returns this ticket\'s administrative Ccs as an RT::Watchers object - -=cut - -sub AdminCc { - my $self = shift; - - my $admincc = $self->Watchers(); - if ($self->CurrentUserHasRight('ShowTicket')) { - $admincc->LimitToAdminCc(); - } - return($admincc); -} - -# }}} - -# }}} - -# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc - -# {{{ sub IsWatcher -# a generic routine to be called by IsRequestor, IsCc and IsAdminCc - -=head2 IsWatcher - -Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string -is a ticket watcher. Returns undef otherwise - -=cut - -sub IsWatcher { - my $self = shift; - - my %args = ( Type => 'Requestor', - Email => undef, - Id => undef, - @_ - ); - - my %cols = ('Type' => $args{'Type'}, - 'Scope' => 'Ticket', - 'Value' => $self->Id, - 'Owner' => undef, - 'Email' => undef - ); - - if (ref($args{'Id'})){ - #If it's a ref, it's an RT::User object; - $cols{'Owner'} = $args{'Id'}->Id; - } - elsif ($args{'Id'} =~ /^\d+$/) { - # if it's an integer, it's a reference to an RT::User obj - $cols{'Owner'} = $args{'Id'}; - } - else { - $cols{'Email'} = $args{'Id'}; - } - - if ($args{'Email'}) { - $cols{'Email'} = $args{'Email'}; - } - - my $description = join(":",%cols); - - #If we've cached a positive match... - if (defined $self->{'watchers_cache'}->{"$description"}) { - if ($self->{'watchers_cache'}->{"$description"} == 1) { - return(1); - } - else { #If we've cached a negative match... - return(undef); - } - } - - - my $watcher = new RT::Watcher($self->CurrentUser); - $watcher->LoadByCols(%cols); - - - if ($watcher->id) { - $self->{'watchers_cache'}->{"$description"} = 1; - return(1); - } - else { - $self->{'watchers_cache'}->{"$description"} = 0; - return(undef); - } - -} -# }}} - -# {{{ sub IsRequestor - -=head2 IsRequestor - - Takes an email address, RT::User object or integer (RT user id) - Returns true if the string is a requestor of the current ticket. - - -=cut - -sub IsRequestor { - my $self = shift; - my $person = shift; - - return ($self->IsWatcher(Type => 'Requestor', Id => $person)); - -}; - -# }}} - -# {{{ sub IsCc - -=head2 IsCc - -Takes a string. Returns true if the string is a Cc watcher of the current ticket. - -=cut - -sub IsCc { - my $self = shift; - my $cc = shift; - - return ($self->IsWatcher( Type => 'Cc', Id => $cc )); - -} - -# }}} - -# {{{ sub IsAdminCc - -=head2 IsAdminCc - -Takes a string. Returns true if the string is an AdminCc watcher of the current ticket. - -=cut - -sub IsAdminCc { - my $self = shift; - my $person = shift; - - return ($self->IsWatcher( Type => 'AdminCc', Id => $person )); - -} - -# }}} - -# {{{ sub IsOwner - -=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); - } -} - - -# }}} - -# }}} - -# }}} - -# {{{ Routines dealing with queues - -# {{{ sub ValidateQueue - -sub ValidateQueue { - my $self = shift; - my $Value = shift; - - #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue, - 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 - -sub SetQueue { - my $self = shift; - my $NewQueue = shift; - - #Redundant. ACL gets checked in _Set; - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - - my $NewQueueObj = RT::Queue->new($self->CurrentUser); - $NewQueueObj->Load($NewQueue); - - unless ($NewQueueObj->Id()) { - return (0, "That queue does not exist"); - } - - if ($NewQueueObj->Id == $self->QueueObj->Id) { - return (0, 'That is the same value'); - } - unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket', - QueueObj => $NewQueueObj )) { - return (0, "You may not create requests in that queue."); - } - - unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket', - QueueObj => $NewQueueObj)) { - $self->Untake(); - } - - return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id())); - -} - -# }}} - -# {{{ sub QueueObj - -=head2 QueueObj - -Takes nothing. returns this ticket's queue object =cut sub QueueObj { - my $self = shift; - - my $queue_obj = RT::Queue->new($self->CurrentUser); - #We call __Value so that we can avoid the ACL decision and some deep recursion - my ($result) = $queue_obj->Load($self->__Value('Queue')); - return ($queue_obj); + my $self = shift; + my $Queue = RT::Queue->new($self->CurrentUser); + $Queue->Load($self->__Value('Queue')); + return($Queue); } +=item Type -# }}} +Returns the current value of Type. +(In the database, Type is stored as varchar(16).) -# }}} -# {{{ Date printing routines -# {{{ sub DueObj - -=head2 DueObj - - Returns an RT::Date object containing this ticket's due date - -=cut -sub DueObj { - my $self = shift; - - my $time = new RT::Date($self->CurrentUser); - - # -1 is RT::Date slang for never - if ($self->Due) { - $time->Set(Format => 'sql', Value => $self->Due ); - } - else { - $time->Set(Format => 'unix', Value => -1); - } - - return $time; -} -# }}} +=item SetType VALUE -# {{{ sub DueAsString -=head2 DueAsString +Set Type to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Type will be stored as a varchar(16).) -Returns this ticket's due date as a human readable string =cut -sub DueAsString { - my $self = shift; - return $self->DueObj->AsString(); -} - -# }}} -# {{{ sub GraceTimeAsString +=item IssueStatement -=head2 GraceTimeAsString +Returns the current value of IssueStatement. +(In the database, IssueStatement is stored as int(11).) -Return the time until this ticket is due as a string - -=cut - -# TODO This should be deprecated - -sub GraceTimeAsString { - my $self=shift; - - if ($self->Due) { - return ($self->DueObj->AgeAsString()); - } else { - return ""; - } -} -# }}} +=item SetIssueStatement VALUE -# {{{ sub ResolvedObj -=head2 ResolvedObj +Set IssueStatement to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, IssueStatement will be stored as a int(11).) - Returns an RT::Date object of this ticket's 'resolved' time. =cut -sub ResolvedObj { - my $self = shift; - my $time = new RT::Date($self->CurrentUser); - $time->Set(Format => 'sql', Value => $self->Resolved); - return $time; -} -# }}} +=item Resolution -# {{{ sub SetStarted +Returns the current value of Resolution. +(In the database, Resolution is stored as int(11).) -=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, "Permission Denied"); - } - - #We create a date object to catch date weirdness - my $time_obj = new RT::Date($self->CurrentUser()); - if ($time != 0) { - $time_obj->Set(Format => 'ISO', Value => $time); - } - else { - $time_obj->SetToNow(); - } - - #Now that we're starting, open this ticket - #TODO do we really want to force this as policy? it should be a scrip - - #We need $TicketAsSystem, in case the current user doesn't have - #ShowTicket - # - my $TicketAsSystem = new RT::Ticket($RT::SystemUser); - $TicketAsSystem->Load($self->Id); - if ($TicketAsSystem->Status eq 'new') { - $TicketAsSystem->Open(); - } - - return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO)); - -} +=item SetResolution VALUE -# }}} -# {{{ sub StartedObj +Set Resolution to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Resolution will be stored as a int(11).) -=head2 StartedObj - - Returns an RT::Date object which contains this ticket's -'Started' time. =cut -sub StartedObj { - my $self = shift; - - my $time = new RT::Date($self->CurrentUser); - $time->Set(Format => 'sql', Value => $self->Started); - return $time; -} -# }}} +=item Owner -# {{{ sub StartsObj +Returns the current value of Owner. +(In the database, Owner is stored as int(11).) -=head2 StartsObj - Returns an RT::Date object which contains this ticket's -'Starts' time. -=cut - -sub StartsObj { - my $self = shift; - - my $time = new RT::Date($self->CurrentUser); - $time->Set(Format => 'sql', Value => $self->Starts); - return $time; -} -# }}} +=item SetOwner VALUE -# {{{ sub ToldObj -=head2 ToldObj +Set Owner to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Owner will be stored as a int(11).) - Returns an RT::Date object which contains this ticket's -'Told' time. =cut -sub ToldObj { - my $self = shift; - - my $time = new RT::Date($self->CurrentUser); - $time->Set(Format => 'sql', Value => $self->Told); - return $time; -} +=item Subject -# }}} +Returns the current value of Subject. +(In the database, Subject is stored as varchar(200).) -# {{{ sub LongSinceToldAsString -# TODO this should be deprecated +=item SetSubject VALUE -sub LongSinceToldAsString { - my $self = shift; - - if ($self->Told) { - return $self->ToldObj->AgeAsString(); - } else { - return "Never"; - } -} -# }}} - -# {{{ sub ToldAsString -=head2 ToldAsString +Set Subject to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Subject will be stored as a varchar(200).) -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"); - } -} -# }}} - -# {{{ sub TimeWorkedAsString +=item InitialPriority -=head2 TimeWorkedAsString +Returns the current value of InitialPriority. +(In the database, InitialPriority is stored as int(11).) -Returns the amount of time worked on this ticket as a Text String -=cut -sub TimeWorkedAsString { - my $self=shift; - return "0" unless $self->TimeWorked; - - #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. - - my $worked = new RT::Date($self->CurrentUser); - #return the #of minutes worked turned into seconds and written as - # a simple text string - - return($worked->DurationAsString($self->TimeWorked*60)); -} +=item SetInitialPriority VALUE -# }}} +Set InitialPriority to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, InitialPriority will be stored as a int(11).) -# }}} - -# {{{ Routines dealing with correspondence/comments - -# {{{ sub Comment - -=head2 Comment - -Comment on this ticket. -Takes a hashref with the follwoing attributes: - -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo =cut -sub Comment { - my $self = shift; - - my %args = ( - CcMessageTo => undef, - BccMessageTo => undef, - MIMEObj => undef, - TimeTaken => 0, - @_ ); - - unless (($self->CurrentUserHasRight('CommentOnTicket')) or - ($self->CurrentUserHasRight('ModifyTicket'))) { - return (0, "Permission Denied"); - } - - unless ($args{'MIMEObj'}) { - return(0,"No correspondence attached"); - } - - # 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 - - $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); - $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); - - #Record the correspondence (write the transaction) - my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment', - Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'), - TimeTaken => $args{'TimeTaken'}, - MIMEObj => $args{'MIMEObj'} - ); - - - return ($Trans, "The comment has been recorded"); -} - -# }}} -# {{{ sub Correspond +=item FinalPriority -=head2 Correspond +Returns the current value of FinalPriority. +(In the database, FinalPriority is stored as int(11).) -Correspond on this ticket. -Takes a hashref with the following attributes: - - -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo - -=cut - -sub Correspond { - my $self = shift; - my %args = ( - CcMessageTo => undef, - BccMessageTo => undef, - MIMEObj => undef, - TimeTaken => 0, - @_ ); - - unless (($self->CurrentUserHasRight('ReplyToTicket')) or - ($self->CurrentUserHasRight('ModifyTicket'))) { - return (0, "Permission Denied"); - } - - unless ($args{'MIMEObj'}) { - return(0,"No correspondence attached"); - } - - # 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 - - $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); - $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); - - #Record the correspondence (write the transaction) - my ($Trans,$msg, $TransObj) = $self->_NewTransaction - (Type => 'Correspond', - Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'), - TimeTaken => $args{'TimeTaken'}, - MIMEObj=> $args{'MIMEObj'} - ); - - # TODO this bit of logic should really become a scrip for 2.2 - my $TicketAsSystem = new RT::Ticket($RT::SystemUser); - $TicketAsSystem->Load($self->Id); - - if ( - ($TicketAsSystem->Status ne 'open') and - ($TicketAsSystem->Status ne 'new') - ) { - - my $oldstatus = $TicketAsSystem->Status(); - $TicketAsSystem->__Set(Field => 'Status', Value => 'open'); - $TicketAsSystem->_NewTransaction - ( Type => 'Set', - Field => 'Status', - OldValue => $oldstatus, - NewValue => 'open', - Data => 'Ticket auto-opened on incoming correspondence' - ); - } - - unless ($Trans) { - $RT::Logger->err("$self couldn't init a transaction ($msg)\n"); - return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'}); - } - - #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 ($TransObj->IsInbound) { - $self->_SetTold; - } - - return ($Trans, "correspondence sent"); -} -# }}} -# }}} +=item SetFinalPriority VALUE -# {{{ Routines dealing with Links and Relations between tickets -# {{{ Link Collections +Set FinalPriority to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, FinalPriority will be stored as a int(11).) -# {{{ sub Members - -=head2 Members - - This returns an RT::Links object which references all the tickets -which are 'MembersOf' this ticket =cut -sub Members { - my $self = shift; - return ($self->_Links('Target', 'MemberOf')); -} - -# }}} -# {{{ sub MemberOf +=item Priority -=head2 MemberOf +Returns the current value of Priority. +(In the database, Priority is stored as int(11).) - This returns an RT::Links object which references all the tickets that this -ticket is a 'MemberOf' -=cut - -sub MemberOf { - my $self = shift; - return ($self->_Links('Base', 'MemberOf')); -} -# }}} +=item SetPriority VALUE -# {{{ RefersTo -=head2 RefersTo +Set Priority to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Priority will be stored as a int(11).) - This returns an RT::Links object which shows all references for which this ticket is a base =cut -sub RefersTo { - my $self = shift; - return ($self->_Links('Base', 'RefersTo')); -} - -# }}} -# {{{ ReferredToBy +=item TimeEstimated -=head2 ReferredToBy +Returns the current value of TimeEstimated. +(In the database, TimeEstimated is stored as int(11).) - This returns an RT::Links object which shows all references for which this ticket is a target -=cut -sub ReferredToBy { - my $self = shift; - return ($self->_Links('Target', 'RefersTo')); -} +=item SetTimeEstimated VALUE -# }}} -# {{{ DependedOnBy +Set TimeEstimated to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, TimeEstimated will be stored as a int(11).) -=head2 DependedOnBy - - This returns an RT::Links object which references all the tickets that depend on this one =cut -sub DependedOnBy { - my $self = shift; - return ($self->_Links('Target','DependsOn')); -} - -# }}} - -# {{{ DependsOn - -=head2 DependsOn - - This returns an RT::Links object which references all the tickets that this ticket depends on - -=cut -sub DependsOn { - my $self = shift; - return ($self->_Links('Base','DependsOn')); -} - -# }}} - -# {{{ sub _Links - -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 || ""; - - unless ($self->{"$field$type"}) { - $self->{"$field$type"} = new RT::Links($self->CurrentUser); - if ($self->CurrentUserHasRight('ShowTicket')) { - - $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI); - $self->{"$field$type"}->Limit(FIELD=>'Type', - VALUE=>$type) if ($type); - } - } - return ($self->{"$field$type"}); -} - -# }}} - -# }}} -# {{{ sub DeleteLink +=item TimeWorked -=head2 DeleteLink +Returns the current value of TimeWorked. +(In the database, TimeWorked is stored as int(11).) -Delete a link. takes a paramhash of Base, Target and Type. -Either Base or Target must be null. The null value will -be replaced with this ticket\'s id -=cut -sub DeleteLink { - my $self = shift; - my %args = ( Base => undef, - Target => undef, - Type => undef, - @_ ); - - #check acls - unless ($self->CurrentUserHasRight('ModifyTicket')) { - $RT::Logger->debug("No permission to delete links\n"); - return (0, 'Permission Denied'); - - - } - - #we want one of base and target. we don't care which - #but we only want _one_ - - if ($args{'Base'} and $args{'Target'}) { - $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n"); - return (0, 'Can\'t specifiy both base and target'); - } - elsif ($args{'Base'}) { - $args{'Target'} = $self->Id(); - } - elsif ($args{'Target'}) { - $args{'Base'} = $self->Id(); - } - else { - $RT::Logger->debug("$self: Base or Target must be specified\n"); - return (0, 'Either base or target must be specified'); - } - - my $link = new RT::Link($self->CurrentUser); - $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n"); - - $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); - - - - #it's a real link. - if ($link->id) { - $RT::Logger->debug("We're going to delete link ".$link->id."\n"); - $link->Delete(); - - my $TransString= - "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}."; - my ($Trans, $Msg, $TransObj) = $self->_NewTransaction - (Type => 'DeleteLink', - Field => $args{'Type'}, - Data => $TransString, - TimeTaken => 0 - ); - - return ($linkid, "Link deleted ($TransString)", $transactionid); - } - #if it's not a link we can find - else { - $RT::Logger->debug("Couldn't find that link\n"); - return (0, "Link not found"); - } -} +=item SetTimeWorked VALUE -# }}} -# {{{ sub AddLink - -=head2 AddLink - -Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket. +Set TimeWorked to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, TimeWorked will be stored as a int(11).) =cut -sub AddLink { - my $self = shift; - my %args = ( Target => '', - Base => '', - Type => '', - @_ ); - - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - if ($args{'Base'} and $args{'Target'}) { - $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n"); - return (0, 'Can\'t specifiy both base and target'); - } - elsif ($args{'Base'}) { - $args{'Target'} = $self->Id(); - } - elsif ($args{'Target'}) { - $args{'Base'} = $self->Id(); - } - else { - return (0, 'Either base or target must be specified'); - } - - # {{{ We don't want references to ourself - if ($args{Base} eq $args{Target}) { - return (0, "Can\'t link a ticket to itself"); - } - - # }}} - - # If the base isn't a URI, make it a URI. - # If the target isn't a URI, make it a URI. - - # {{{ Check if the link already exists - we don't want duplicates - my $old_link= new RT::Link ($self->CurrentUser); - $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); - if ($old_link->Id) { - $RT::Logger->debug("$self Somebody tried to duplicate a link"); - return ($old_link->id, "Link already exists",0); - } - # }}} - - # Storing the link in the DB. - my $link = RT::Link->new($self->CurrentUser); - my ($linkid) = $link->Create(Target => $args{Target}, - Base => $args{Base}, - Type => $args{Type}); - - unless ($linkid) { - return (0,"Link could not be created"); - } - #Write the transaction - - my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}."; - - my ($Trans, $Msg, $TransObj) = $self->_NewTransaction - (Type => 'AddLink', - Field => $args{'Type'}, - Data => $TransString, - TimeTaken => 0 - ); - - return ($Trans, "Link created ($TransString)"); - - -} -# }}} -# {{{ sub URI +=item Status -=head2 URI +Returns the current value of Status. +(In the database, Status is stored as varchar(10).) -Returns this ticket's URI -=cut -sub URI { - my $self = shift; - return $RT::TicketBaseURI.$self->id; -} +=item SetStatus VALUE -# }}} -# {{{ sub MergeInto +Set Status to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Status will be stored as a varchar(10).) -=head2 MergeInto -MergeInto take the id of the ticket to merge this ticket into. =cut -sub MergeInto { - my $self = shift; - my $MergeInto = shift; - - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - # Load up the new ticket. - my $NewTicket = RT::Ticket->new($RT::SystemUser); - $NewTicket->Load($MergeInto); - - # make sure it exists. - unless (defined $NewTicket->Id) { - return (0, 'New ticket doesn\'t exist'); - } - - - # Make sure the current user can modify the new ticket. - unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) { - $RT::Logger->debug("failed..."); - return (0, "Permission Denied"); - } - - $RT::Logger->debug("checking if the new ticket has the same id and effective id..."); - unless ($NewTicket->id == $NewTicket->EffectiveId) { - $RT::Logger->err('$self trying to merge into '.$NewTicket->Id . - ' which is itself merged.\n'); - return (0, "Can't merge into a merged ticket. ". - "You should never get this error"); - } - - - # 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 seperate 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 => $NewTicket->Id()); - - unless ($id_val) { - $RT::Logger->error("Couldn't set effective ID for ".$self->Id. - ": $id_msg"); - return(0,"Merge failed. Couldn't set EffectiveId"); - } - - my ($status_val, $status_msg) = $self->__Set(Field => 'Status', - Value => 'resolved'); - - unless ($status_val) { - $RT::Logger->error("$self couldn't set status to resolved.". - "RT's Database may be inconsistent."); - } - - #make a new link: this ticket is merged into that other ticket. - $self->AddLink( Type =>'MergedInto', - Target => $NewTicket->Id() ); - - #add all of this ticket's watchers to that ticket. - my $watchers = $self->Watchers(); - - while (my $watcher = $watchers->Next()) { - unless ( - ($watcher->Owner && - $NewTicket->IsWatcher (Type => $watcher->Type, - Id => $watcher->Owner)) or - ($watcher->Email && - $NewTicket->IsWatcher (Type => $watcher->Type, - Id => $watcher->Email)) - ) { - - - - $NewTicket->_AddWatcher(Silent => 1, - Type => $watcher->Type, - Email => $watcher->Email, - Owner => $watcher->Owner); - } - } - - - #find all of the tickets that were merged into this ticket. - my $old_mergees = new RT::Tickets($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 => $NewTicket->Id()); - } - $NewTicket->_SetLastUpdated; - - return ($TransactionObj, "Merge Successful"); -} - -# }}} - -# }}} - -# {{{ Routines dealing with keywords - -# {{{ sub KeywordsObj - -=head2 KeywordsObj [KEYWORD_SELECT_ID] - - Returns an B object preloaded with this ticket's ObjectKeywords. -If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword -select. - -=cut - -sub KeywordsObj { - my $self = shift; - my $keyword_select; - - $keyword_select = shift if (@_); - - use RT::ObjectKeywords; - my $Keywords = new RT::ObjectKeywords($self->CurrentUser); - - #ACL check - if ($self->CurrentUserHasRight('ShowTicket')) { - $Keywords->LimitToTicket($self->id); - if ($keyword_select) { - $Keywords->LimitToKeywordSelect($keyword_select); - } - } - return ($Keywords); -} -# }}} -# {{{ sub AddKeyword +=item TimeLeft -=head2 AddKeyword +Returns the current value of TimeLeft. +(In the database, TimeLeft is stored as int(11).) -Takes a paramhash of Keyword and KeywordSelect. If Keyword is a valid choice -for KeywordSelect, creates a KeywordObject. If the KeywordSelect says this should -be a single KeywordObject, automatically removes the old value. - Issues: probably doesn't enforce the depth restrictions or make sure that keywords -are coming from the right part of the tree. really should. -=cut +=item SetTimeLeft VALUE -sub AddKeyword { - my $self = shift; - #ACL check - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, 'Permission Denied'); - } - - return($self->_AddKeyword(@_)); - -} +Set TimeLeft to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, TimeLeft will be stored as a int(11).) -# Helper version of AddKeyword without that pesky ACL check -sub _AddKeyword { - my $self = shift; - my %args = ( KeywordSelect => undef, # id of a keyword select record - Keyword => undef, #id of the keyword to add - Silent => 0, - @_ - ); - - my ($OldValue); - - #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'} - - #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue. - - my $Keyword = new RT::Keyword($self->CurrentUser); - unless ($Keyword->Load($args{'Keyword'}) ) { - $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n"); - return(0, "Couldn't load keyword"); - } - - my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser); - unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) { - $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'}); - return(0, "Couldn't load keywordselect"); - } - - my $Keywords = $self->KeywordsObj($KeywordSelectObj->id); - - #If the ticket already has this keyword, just get out of here. - if ($Keywords->HasEntry($Keyword->id)) { - return(0, "That is already the current value"); - } - - #If the keywordselect wants this to be a singleton: - - if ($KeywordSelectObj->Single) { - - #Whack any old values...keep track of the last value that we get. - #we shouldn't need a loop ehre, but we do it anyway, to try to - # help keep the database clean. - while (my $OldKey = $Keywords->Next) { - $OldValue = $OldKey->KeywordObj->Name; - $OldKey->Delete(); - } - - - } - - # create the new objectkeyword - my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); - my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id, - ObjectType => 'Ticket', - ObjectId => $self->Id, - KeywordSelect => $KeywordSelectObj->Id ); - - - # record a single transaction, unless we were told not to - unless ($args{'Silent'}) { - my ($TransactionId, $Msg, $TransactionObj) = - $self->_NewTransaction( Type => 'Keyword', - Field => $KeywordSelectObj->Id, - OldValue => $OldValue, - NewValue => $Keyword->Name ); - } - return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added."); - -} - -# }}} - -# {{{ sub DeleteKeyword - -=head2 DeleteKeyword - - Takes a paramhash. Deletes the Keyword denoted by the I parameter from this - ticket's object keywords. =cut -sub DeleteKeyword { - my $self = shift; - my %args = ( Keyword => undef, - KeywordSelect => undef, - @_ ); - - #ACL check - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, 'Permission Denied'); - } - - - #Load up the ObjectKeyword we\'re talking about - my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); - $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'}, - KeywordSelect => $args{'KeywordSelect'}, - ObjectType => 'Ticket', - ObjectId => $self->id() - ); - - #if we can\'t find it, bail - unless ($ObjectKeyword->id) { - $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} . - " for keywordselect ". $args{'KeywordSelect'} . - "for ticket ".$self->id ); - return (undef, "Couldn't load keyword while trying to delete it."); - }; - - #record transaction here. - my ($TransactionId, $Msg, $TransObj) = - $self->_NewTransaction( Type => 'Keyword', - OldValue => $ObjectKeyword->KeywordObj->Name); - - $ObjectKeyword->Delete(); - - return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted."); - -} -# }}} +=item Told -# }}} +Returns the current value of Told. +(In the database, Told is stored as datetime.) -# {{{ Routines dealing with ownership -# {{{ sub OwnerObj -=head2 OwnerObj +=item SetTold VALUE -Takes nothing and returns an RT::User object of -this ticket's owner -=cut +Set Told to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Told will be stored as a datetime.) -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 - - $owner = new RT::User ($self->CurrentUser); - $owner->Load($self->__Value('Owner')); - - #Return the owner object - return ($owner); -} - -# }}} - -# {{{ sub OwnerAsString - -=head2 OwnerAsString - -Returns the owner's email address =cut -sub OwnerAsString { - my $self = shift; - return($self->OwnerObj->EmailAddress); -} +=item Starts -# }}} +Returns the current value of Starts. +(In the database, Starts is stored as datetime.) -# {{{ sub SetOwner -=head2 SetOwner - -Takes two arguments: - the Id or Name of the owner -and (optionally) the type of the SetOwner Transaction. It defaults -to 'Give'. 'Steal' is also a valid option. - -=cut - -sub SetOwner { - my $self = shift; - my $NewOwner = shift; - my $Type = shift || "Give"; - - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - my $NewOwnerObj = RT::User->new($self->CurrentUser); - my $OldOwnerObj = $self->OwnerObj; - - $NewOwnerObj->Load($NewOwner); - if (!$NewOwnerObj->Id) { - return (0, "That user does not exist"); - } - - #If thie ticket has an owner and it's not the current user - - if (($Type ne 'Steal' ) and ($Type ne 'Force') and #If we're not stealing - ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set - ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us - return(0, "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->Id) and - (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket', - QueueObj => $self->QueueObj, - TicketObj => $self)) - ) { - return (0, "That user may not own requests in that queue"); - } - - - #If the ticket has an owner and it's the new owner, we don't need - #To do anything - elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) { - return(0, "That user already owns that request"); - } - - - my ($trans,$msg)=$self->_Set(Field => 'Owner', - Value => $NewOwnerObj->Id, - TimeTaken => 0, - TransactionType => $Type); - - if ($trans) { - $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name; - } - return ($trans, $msg); - -} -# }}} +=item SetStarts VALUE -# {{{ sub Take -=head2 Take +Set Starts to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Starts will be stored as a datetime.) -A convenince method to set the ticket's owner to the current user =cut -sub Take { - my $self = shift; - return ($self->SetOwner($self->CurrentUser->Id, 'Take')); -} -# }}} +=item Started -# {{{ sub Untake +Returns the current value of Started. +(In the database, Started is stored as datetime.) -=head2 Untake -Convenience method to set the owner to 'nobody' if the current user is the owner. -=cut +=item SetStarted VALUE -sub Untake { - my $self = shift; - return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake')); -} -# }}} - -# {{{ sub Steal -=head2 Steal +Set Started to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Started will be stored as a datetime.) -A convenience method to change the owner of the current ticket to the -current user. Even if it's owned by another user. =cut -sub Steal { - my $self = shift; - - if ($self->IsOwner($self->CurrentUser)) { - return (0,"You already own this ticket"); - } else { - return($self->SetOwner($self->CurrentUser->Id, 'Steal')); - - } - -} - -# }}} - -# }}} -# {{{ Routines dealing with status +=item Due -# {{{ sub ValidateStatus +Returns the current value of Due. +(In the database, Due is stored as datetime.) -=head2 ValidateStatus STATUS -Takes a string. Returns true if that status is a valid status for this ticket. -Returns false otherwise. -=cut - -sub ValidateStatus { - my $self = shift; - my $status = shift; +=item SetDue VALUE - #Make sure the status passed in is valid - unless ($self->QueueObj->IsValidStatus($status)) { - return (undef); - } - - return (1); -} +Set Due to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Due will be stored as a datetime.) -# }}} +=cut -# {{{ sub SetStatus -=head2 SetStatus STATUS +=item Resolved -Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead. +Returns the current value of Resolved. +(In the database, Resolved is stored as datetime.) -=cut -sub SetStatus { - my $self = shift; - my $status = shift; - - #Check ACL - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, 'Permission Denied'); - } - - my $now = new RT::Date($self->CurrentUser); - $now->SetToNow(); - - #If we're changing the status from new, record that we've started - if (($self->Status =~ /new/) && ($status ne 'new')) { - #Set the Started time to "now" - $self->_Set(Field => 'Started', - Value => $now->ISO, - RecordTransaction => 0); - } - - - if ($status eq 'resolved') { - #When we resolve a ticket, set the 'Resolved' attribute to now. - $self->_Set(Field => 'Resolved', - Value => $now->ISO, - RecordTransaction => 0); - } - - - #Actually update the status - return($self->_Set(Field => 'Status', - Value => $status, - TimeTaken => 0, - TransactionType => 'Status')); -} -# }}} +=item SetResolved VALUE -# {{{ sub Kill -=head2 Kill +Set Resolved to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Resolved will be stored as a datetime.) -Takes no arguments. Marks this ticket for garbage collection =cut -sub Kill { - my $self = shift; - return ($self->SetStatus('dead')); - # TODO: garbage collection -} - -# }}} -# {{{ sub Stall +=item LastUpdatedBy -=head2 Stall +Returns the current value of LastUpdatedBy. +(In the database, LastUpdatedBy is stored as int(11).) -Sets this ticket's status to stalled =cut -sub Stall { - my $self = shift; - return ($self->SetStatus('stalled')); -} - -# }}} -# {{{ sub Open +=item LastUpdated -=head2 Open +Returns the current value of LastUpdated. +(In the database, LastUpdated is stored as datetime.) -Sets this ticket\'s status to Open =cut -sub Open { - my $self = shift; - return ($self->SetStatus('open')); -} - -# }}} -# {{{ sub Resolve +=item Creator -=head2 Resolve +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) -Sets this ticket\'s status to Resolved =cut -sub Resolve { - my $self = shift; - return ($self->SetStatus('resolved')); -} - -# }}} - -# }}} - -# {{{ Actions + Routines dealing with transactions -# {{{ sub SetTold and _SetTold +=item Created -=head2 SetTold ISO [TIMETAKEN] +Returns the current value of Created. +(In the database, Created is stored as datetime.) -Updates the told and records a transaction =cut -sub SetTold { - my $self=shift; - my $told; - $told = shift if (@_); - my $timetaken=shift || 0; - - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - my $datetold = new RT::Date($self->CurrentUser); - if ($told) { - $datetold->Set( Format => 'iso', - Value => $told); - } - else { - $datetold->SetToNow(); - } - - return($self->_Set(Field => 'Told', - Value => $datetold->ISO, - TimeTaken => $timetaken, - TransactionType => 'Told')); -} -=head2 _SetTold +=item Disabled -Updates the told without a transaction or acl check. Useful when we're sending replies. +Returns the current value of Disabled. +(In the database, Disabled is stored as smallint(6).) -=cut -sub _SetTold { - my $self=shift; - - my $now = new RT::Date($self->CurrentUser); - $now->SetToNow(); - #use __Set to get no ACLs ;) - return($self->__Set(Field => 'Told', - Value => $now->ISO)); -} -# }}} +=item SetDisabled VALUE -# {{{ sub Transactions -=head2 Transactions +Set Disabled to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Disabled will be stored as a smallint(6).) - Returns an RT::Transactions object of all transactions on this ticket =cut - -sub Transactions { - my $self = shift; - - use RT::Transactions; - my $transactions = RT::Transactions->new($self->CurrentUser); - - #If the user has no rights, return an empty object - if ($self->CurrentUserHasRight('ShowTicket')) { - my $tickets = $transactions->NewAlias('Tickets'); - $transactions->Join( ALIAS1 => 'main', - FIELD1 => 'Ticket', - ALIAS2 => $tickets, - FIELD2 => 'id'); - $transactions->Limit( ALIAS => $tickets, - FIELD => 'EffectiveId', - VALUE => $self->id()); - # if the user may not see comments do not return them - unless ($self->CurrentUserHasRight('ShowTicketComments')) { - $transactions->Limit( FIELD => 'Type', - OPERATOR => '!=', - VALUE => "Comment"); - } - } - - return($transactions); -} - -# }}} - -# {{{ sub _NewTransaction - -sub _NewTransaction { - my $self = shift; - my %args = ( TimeTaken => 0, - Type => undef, - OldValue => undef, - NewValue => undef, - Data => undef, - Field => undef, - MIMEObj => undef, - @_ ); - - - require RT::Transaction; - my $trans = new RT::Transaction($self->CurrentUser); - my ($transaction, $msg) = - $trans->Create( Ticket => $self->Id, - TimeTaken => $args{'TimeTaken'}, - Type => $args{'Type'}, - Data => $args{'Data'}, - Field => $args{'Field'}, - NewValue => $args{'NewValue'}, - OldValue => $args{'OldValue'}, - MIMEObj => $args{'MIMEObj'} - ); - - $RT::Logger->warning($msg) unless $transaction; - - $self->_SetLastUpdated; - - if (defined $args{'TimeTaken'} ) { - $self->_UpdateTimeTaken($args{'TimeTaken'}); - } - return($transaction, $msg, $trans); -} -# }}} -# }}} - -# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record - -# {{{ sub _ClassAccessible sub _ClassAccessible { { - EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 }, - Queue => { 'read' => 1, 'write' => 1 }, - Requestors => { 'read' => 1, 'write' => 1 }, - Owner => { 'read' => 1, 'write' => 1 }, - Subject => { 'read' => 1, 'write' => 1 }, - InitialPriority => { 'read' => 1, 'write' => 1 }, - FinalPriority => { 'read' => 1, 'write' => 1 }, - Priority => { 'read' => 1, 'write' => 1 }, - Status => { 'read' => 1, 'write' => 1 }, - TimeWorked => { 'read' => 1, 'write' => 1 }, - TimeLeft => { 'read' => 1, 'write' => 1 }, - Created => { 'read' => 1, 'auto' => 1 }, - Creator => { 'read' => 1, 'auto' => 1 }, - Told => { 'read' => 1, 'write' => 1 }, - Resolved => {'read' => 1}, - Starts => { 'read' => 1, 'write' => 1 }, - Started => { 'read' => 1, 'write' => 1 }, - Due => { 'read' => 1, 'write' => 1 }, - Creator => { 'read' => 1, 'auto' => 1 }, - Created => { 'read' => 1, 'auto' => 1 }, - LastUpdatedBy => { 'read' => 1, 'auto' => 1 }, - LastUpdated => { 'read' => 1, 'auto' => 1 } - }; - -} - -# }}} - -# {{{ sub _Set - -sub _Set { - my $self = shift; - - unless ($self->CurrentUserHasRight('ModifyTicket')) { - return (0, "Permission Denied"); - } - - my %args = (Field => undef, - Value => undef, - TimeTaken => 0, - RecordTransaction => 1, - TransactionType => 'Set', - @_ - ); - #if the user is trying to modify the record - - #Take care of the old value we really don't want to get in an ACL loop. - # so ask the super::_Value - my $Old=$self->SUPER::_Value("$args{'Field'}"); - - #Set the new value - my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, - Value=> $args{'Value'}); - - #If we can't actually set the field to the value, don't record - # a transaction. instead, get out of here. - if ($ret==0) {return (0,$msg);} - - if ($args{'RecordTransaction'} == 1) { - - my ($Trans, $Msg, $TransObj) = - $self->_NewTransaction(Type => $args{'TransactionType'}, - Field => $args{'Field'}, - NewValue => $args{'Value'}, - OldValue => $Old, - TimeTaken => $args{'TimeTaken'}, - ); - return ($Trans,$TransObj->Description); - } - else { - return ($ret, $msg); - } -} - -# }}} - -# {{{ sub _Value - -=head2 _Value - -Takes the name of a table column. -Returns its value as a string, if the user passes an ACL check - -=cut - -sub _Value { - - my $self = shift; - my $field = shift; - - - #if the field is public, return it. - if ($self->_Accessible($field, 'public')) { - #$RT::Logger->debug("Skipping ACL check for $field\n"); - return($self->SUPER::_Value($field)); - - } - - #If the current user doesn't have ACLs, don't let em at it. - - unless ($self->CurrentUserHasRight('ShowTicket')) { - return (undef); - } - return($self->SUPER::_Value($field)); - -} - -# }}} - -# {{{ sub _UpdateTimeTaken - -=head2 _UpdateTimeTaken - -This routine will increment the timeworked counter. it should -only be called from _NewTransaction - -=cut - -sub _UpdateTimeTaken { - my $self = shift; - my $Minutes = shift; - my ($Total); - - $Total = $self->SUPER::_Value("TimeWorked"); - $Total = ($Total || 0) + ($Minutes || 0); - $self->SUPER::_Set(Field => "TimeWorked", - Value => $Total); - - return ($Total); -} + + id => + {read => 1, type => 'int(11)', default => ''}, + EffectiveId => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Queue => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Type => + {read => 1, write => 1, type => 'varchar(16)', default => ''}, + IssueStatement => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Resolution => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Owner => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Subject => + {read => 1, write => 1, type => 'varchar(200)', default => '[no subject]'}, + InitialPriority => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + FinalPriority => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Priority => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + TimeEstimated => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + TimeWorked => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Status => + {read => 1, write => 1, type => 'varchar(10)', default => ''}, + TimeLeft => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Told => + {read => 1, write => 1, type => 'datetime', default => ''}, + Starts => + {read => 1, write => 1, type => 'datetime', default => ''}, + Started => + {read => 1, write => 1, type => 'datetime', default => ''}, + Due => + {read => 1, write => 1, type => 'datetime', default => ''}, + Resolved => + {read => 1, write => 1, type => 'datetime', default => ''}, + LastUpdatedBy => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + LastUpdated => + {read => 1, auto => 1, type => 'datetime', default => ''}, + Creator => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, type => 'datetime', default => ''}, + Disabled => + {read => 1, write => 1, type => 'smallint(6)', default => '0'}, + + } +}; -# }}} -# }}} + eval "require RT::Ticket_Overlay"; + if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Overlay.pm}) { + die $@; + }; -# {{{ Routines dealing with ACCESS CONTROL + eval "require RT::Ticket_Vendor"; + if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Vendor.pm}) { + die $@; + }; -# {{{ sub CurrentUserHasRight + eval "require RT::Ticket_Local"; + if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Local.pm}) { + die $@; + }; -=head2 CurrentUserHasRight - Takes the textual name of a Ticket scoped right (from RT::ACE) and returns -1 if the user has that right. It returns 0 if the user doesn't have that right. -=cut -sub CurrentUserHasRight { - my $self = shift; - my $right = shift; - - return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(), - Right => "$right")); +=head1 SEE ALSO -} +This class allows "overlay" methods to be placed +into the following files _Overlay is for a System overlay by the original author, +_Vendor is for 3rd-party vendor add-ons, while _Local is for site-local customizations. -# }}} +These overlay files can contain new subs or subs to replace existing subs in this module. -# {{{ sub HasRight +If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line -=head2 HasRight + no warnings qw(redefine); - Takes a paramhash with the attributes 'Right' and 'Principal' - 'Right' is a ticket-scoped textual right from RT::ACE - 'Principal' is an RT::User object +so that perl does not kick and scream when you redefine a subroutine or variable in your overlay. - Returns 1 if the principal has the right. Returns undef if not. +RT::Ticket_Overlay, RT::Ticket_Vendor, RT::Ticket_Local =cut -sub HasRight { - my $self = shift; - my %args = ( Right => undef, - Principal => undef, - @_); - - unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) { - $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight"); - } - - return($args{'Principal'}->HasQueueRight(TicketObj => $self, - Right => $args{'Right'})); -} - -# }}} - -# }}} - 1; - -=head1 AUTHOR - -Jesse Vincent, jesse@fsck.com - -=head1 SEE ALSO - -RT - -=cut - - -- cgit v1.2.1