X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FTicket_Overlay.pm;h=a51cd16877a84ee6e675655dfd908a6ad0c34030;hb=9308beec0b556647bf703b67ba6a036a5bedff37;hp=a294dcafd28fd6b4bc839ba263328d3c37d27963;hpb=5e05724a635a22776f1b973f5d7e77989da4e048;p=freeside.git diff --git a/rt/lib/RT/Ticket_Overlay.pm b/rt/lib/RT/Ticket_Overlay.pm index a294dcafd..a51cd1687 100644 --- a/rt/lib/RT/Ticket_Overlay.pm +++ b/rt/lib/RT/Ticket_Overlay.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -22,7 +22,9 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/copyleft/gpl.html. # # # CONTRIBUTION SUBMISSION POLICY: @@ -151,6 +153,7 @@ use RT::Date; use RT::CustomFields; use RT::Tickets; use RT::Transactions; +use RT::Reminders; use RT::URI::fsck_com_rt; use RT::URI; use MIME::Entity; @@ -327,9 +330,19 @@ Arguments: ARGS is a hash of named parameters. Valid parameters are: MIMEObj -- a MIME::Entity object with the content of the initial ticket request. CustomField- -- a scalar or array of values for the customfield with the id +Ticket links can be set up during create by passing the link type as a hask key and +the ticket id to be linked to as a value (or a URI when linking to other objects). +Multiple links of the same type can be created by passing an array ref. For example: -Returns: TICKETID, Transaction Object, Error Message + Parent => 45, + DependsOn => [ 15, 22 ], + RefersTo => 'http://www.bestpractical.com', + +Supported link types are C, C, C, C, +C and C. Also, C is alias for C and +C and C are aliases for C. +Returns: TICKETID, Transaction Object, Error Message =begin testing @@ -463,10 +476,9 @@ sub Create { #If the status is an inactive status, set the resolved date if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} ) { - $RT::Logger->debug( "Got a " - . $args{'Status'} - . "ticket with a resolved of " - . $args{'Resolved'} ); + $RT::Logger->debug( "Got a ". $args{'Status'} + ." ticket with undefined resolved date. Setting to now." + ); $Resolved->SetToNow; } @@ -678,6 +690,20 @@ sub Create { foreach my $link ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) { + # Check rights on the other end of the link if we must + # then run _AddLink that doesn't check for ACLs + if ( $RT::StrictLinkACL ) { + my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link ); + unless ( $val ) { + push @non_fatal_errors, $msg; + next; + } + if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) { + push @non_fatal_errors, $self->loc('Linking. Permission denied'); + next; + } + } + my ( $wval, $wmsg ) = $self->_AddLink( Type => $LINKTYPEMAP{$type}->{'Type'}, $LINKTYPEMAP{$type}->{'Mode'} => $link, @@ -690,6 +716,68 @@ sub Create { # }}} + # {{{ Deal with auto-customer association + + #unless we already have (a) customer(s)... + unless ( $self->Customers->Count ) { + + #first find any requestors with emails but *without* customer targets + my @NoCust_Requestors = + grep { $_->EmailAddress && ! $_->Customers->Count } + @{ $self->_Requestors->UserMembersObj->ItemsArrayRef }; + + for my $Requestor (@NoCust_Requestors) { + + #perhaps the stuff in here should be in a User method?? + my @Customers = + &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress ); + + foreach my $custnum ( map $_->{'custnum'}, @Customers ) { + + ## false laziness w/RT/Interface/Web_Vendor.pm + my @link = ( 'Type' => 'MemberOf', + 'Target' => "freeside://freeside/cust_main/$custnum", + ); + + my( $val, $msg ) = $Requestor->_AddLink(@link); + #XXX should do something with $msg# push @non_fatal_errors, $msg; + + } + + } + + #find any requestors with customer targets + + my %cust_target = (); + + my @Requestors = + grep { $_->Customers->Count } + @{ $self->_Requestors->UserMembersObj->ItemsArrayRef }; + + foreach my $Requestor ( @Requestors ) { + foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) { + $cust_target{ $cust_link->Target } = 1; + } + } + + #and then auto-associate this ticket with those customers + + foreach my $cust_target ( keys %cust_target ) { + + my @link = ( 'Type' => 'MemberOf', + #'Target' => "freeside://freeside/cust_main/$custnum", + 'Target' => $cust_target, + ); + + my( $val, $msg ) = $self->_AddLink(@link); + push @non_fatal_errors, $msg; + + } + + } + + # }}} + # {{{ Add all the custom fields foreach my $arg ( keys %args ) { @@ -1321,7 +1409,10 @@ sub AddWatcher { # {{{ Check ACLS #If the watcher we're trying to add is for the current user - if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) { + if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0) + or lc( $self->CurrentUser->UserObj->EmailAddress ) + eq lc( RT::User->CanonicalizeEmailAddress( $args{'Email'} ) || '' ) ) + { # If it's an AdminCc and they don't have # 'WatchAsAdminCc' or 'ModifyTicket', bail if ( $args{'Type'} eq 'AdminCc' ) { @@ -1485,7 +1576,7 @@ sub DeleteWatcher { # {{{ Check ACLS #If the watcher we're trying to add is for the current user - if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) { + if ( $self->CurrentUser->PrincipalId == $principal->id ) { # If it's an AdminCc and they don't have # 'WatchAsAdminCc' or 'ModifyTicket', bail @@ -1720,6 +1811,25 @@ sub Requestors { # }}} +# {{{ sub _Requestors + +=head2 _Requestors + +Private non-ACLed variant of Reqeustors so that we can look them up for the +purposes of customer auto-association during create. + +=cut + +sub _Requestors { + my $self = shift: + + my $group = RT::Group->new($RT::SystemUser); + $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id); + return ($group); +} + +% }}} + # {{{ sub Cc =head2 Cc @@ -2096,12 +2206,12 @@ sub SetStarted { my $time = shift || 0; unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, self->loc("Permission Denied") ); + return ( 0, $self->loc("Permission Denied") ); } #We create a date object to catch date weirdness my $time_obj = new RT::Date( $self->CurrentUser() ); - if ( $time != 0 ) { + if ( $time ) { $time_obj->Set( Format => 'ISO', Value => $time ); } else { @@ -2381,21 +2491,23 @@ sub _RecordNote { # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc: # headers - $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress( - undef, $args{'CcMessageTo'} - ) ) - if defined $args{'CcMessageTo'}; - $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', - RT::User::CanonicalizeEmailAddress( - undef, $args{'BccMessageTo'} - ) ) - if defined $args{'BccMessageTo'}; + + foreach my $type (qw/Cc Bcc/) { + if ( defined $args{ $type . 'MessageTo' } ) { + + my $addresses = join ', ', ( + map { RT::User->CanonicalizeEmailAddress( $_->address ) } + Mail::Address->parse( $args{ $type . 'MessageTo' } ) ); + $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses ); + } + } # If this is from an external source, we need to come up with its # internal Message-ID now, so all emails sent because of this # message have a common Message-ID - unless ($args{'MIMEObj'}->head->get('Message-ID') - =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) { + unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '') + =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ ) + { $args{'MIMEObj'}->head->set( 'RT-Message-ID', "{"$field$type"} ) { $self->{"$field$type"} = new RT::Links( $self->CurrentUser ); - if ( $self->CurrentUserHasRight('ShowTicket') ) { + + #not sure what this ACL was supposed to do... but returning the + # bare (unlimited) RT::Links certainly seems wrong, it causes the + # $Ticket->Customers method during creation to return results for every + # ticket... + #if ( $self->CurrentUserHasRight('ShowTicket') ) { + # Maybe this ticket is a merged ticket my $Tickets = new RT::Tickets( $self->CurrentUser ); # at least to myself @@ -2459,7 +2577,7 @@ sub _Links { $self->{"$field$type"}->Limit( FIELD => 'Type', VALUE => $type ) if ($type); - } + #} } return ( $self->{"$field$type"} ); } @@ -2485,11 +2603,29 @@ sub DeleteLink { @_ ); + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } + #check acls - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - $RT::Logger->debug("No permission to delete links\n"); - return ( 0, $self->loc('Permission Denied')) + my $right = 0; + $right++ if $self->CurrentUserHasRight('ModifyTicket'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } + # If the other URI is an RT::Ticket, we want to make sure the user + # can modify it too... + my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); + return (0, $msg) unless $status; + if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { + $right++; + } + if ( ( !$RT::StrictLinkACL && $right == 0 ) || + ( $RT::StrictLinkACL && $right < 2 ) ) + { + return ( 0, $self->loc("Permission Denied") ); } my ($val, $Msg) = $self->SUPER::_DeleteLink(%args); @@ -2557,13 +2693,52 @@ sub AddLink { Silent => undef, @_ ); + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { + my $right = 0; + $right++ if $self->CurrentUserHasRight('ModifyTicket'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } + + # If the other URI is an RT::Ticket, we want to make sure the user + # can modify it too... + my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); + return (0, $msg) unless $status; + if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { + $right++; + } + if ( ( !$RT::StrictLinkACL && $right == 0 ) || + ( $RT::StrictLinkACL && $right < 2 ) ) + { return ( 0, $self->loc("Permission Denied") ); } + return $self->_AddLink(%args); +} + +sub __GetTicketFromURI { + my $self = shift; + my %args = ( URI => '', @_ ); + + # If the other URI is an RT::Ticket, we want to make sure the user + # can modify it too... + my $uri_obj = RT::URI->new( $self->CurrentUser ); + $uri_obj->FromURI( $args{'URI'} ); - $self->_AddLink(%args); + unless ( $uri_obj->Resolver && $uri_obj->Scheme ) { + my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} ); + $RT::Logger->warning( "$msg\n" ); + return( 0, $msg ); + } + my $obj = $uri_obj->Resolver->Object; + unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) { + return (1, 'Found not a ticket', undef); + } + return (1, 'Found ticket', $obj); } =head2 _AddLink @@ -2580,45 +2755,8 @@ sub _AddLink { Silent => undef, @_ ); - # {{{ If the other URI is an RT::Ticket, we want to make sure the user - # can modify it too... - my $other_ticket_uri = RT::URI->new($self->CurrentUser); - - if ( $args{'Target'} ) { - $other_ticket_uri->FromURI( $args{'Target'} ); - - } - elsif ( $args{'Base'} ) { - $other_ticket_uri->FromURI( $args{'Base'} ); - } - - unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) { - my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'}) - : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'}); - $RT::Logger->warning( "$self $msg\n" ); - - return( 0, $msg ); - } - - if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') { - my $object = $other_ticket_uri->Resolver->Object; - - if ( UNIVERSAL::isa( $object, 'RT::Ticket' ) - && $object->id - && !$object->CurrentUserHasRight('ModifyTicket') ) - { - return ( 0, $self->loc("Permission Denied") ); - } - - } - - # }}} - - my ($val, $Msg) = $self->SUPER::_AddLink(%args); - - if (!$val) { - return ($val, $Msg); - } + my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args); + return ($val, $msg) if !$val || $exist; my ($direction, $remote_link); if ( $args{'Target'} ) { @@ -2631,10 +2769,10 @@ sub _AddLink { # Don't write the transaction if we're doing this on create if ( $args{'Silent'} ) { - return ( $val, $Msg ); + return ( $val, $msg ); } else { - my $remote_uri = RT::URI->new( $self->CurrentUser ); + my $remote_uri = RT::URI->new( $self->CurrentUser ); $remote_uri->FromURI( $remote_link ); #Write the transaction @@ -2732,14 +2870,23 @@ sub MergeInto { return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") ); } - my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved'); - unless ($status_val) { - $RT::Handle->Rollback(); - $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) ); - return ( 0, $self->loc("Merge failed. Couldn't set Status") ); - } + if ( $self->__Value('Status') ne 'resolved' ) { + my ( $status_val, $status_msg ) + = $self->__Set( Field => 'Status', Value => 'resolved' ); + + unless ($status_val) { + $RT::Handle->Rollback(); + $RT::Logger->error( + $self->loc( + "[_1] couldn't set status to resolved. RT's Database may be inconsistent.", + $self + ) + ); + return ( 0, $self->loc("Merge failed. Couldn't set Status") ); + } + } # update all the links that point to that old ticket my $old_links_to = RT::Links->new($self->CurrentUser); @@ -2916,6 +3063,8 @@ my $txns = RT::Transactions->new($RT::SystemUser); $txns->OrderBy(FIELD => 'id', ORDER => 'DESC'); $txns->Limit(FIELD => 'ObjectId', VALUE => '1'); $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket'); +$txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord'); + my $steal = $txns->First; ok($steal->OldValue == $root->Id , "Stolen from root"); ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser"); @@ -2929,68 +3078,77 @@ sub SetOwner { my $NewOwner = shift; my $Type = shift || "Give"; + $RT::Handle->BeginTransaction(); + + $self->_SetLastUpdated(); # lock the ticket + $self->Load( $self->id ); # in case $self changed while waiting for lock + + my $OldOwnerObj = $self->OwnerObj; + + my $NewOwnerObj = RT::User->new( $self->CurrentUser ); + $NewOwnerObj->Load( $NewOwner ); + unless ( $NewOwnerObj->Id ) { + $RT::Handle->Rollback(); + return ( 0, $self->loc("That user does not exist") ); + } + + # must have ModifyTicket rights # or TakeTicket/StealTicket and $NewOwner is self # see if it's a take - if ( $self->OwnerObj->Id == $RT::Nobody->Id ) { + if ( $OldOwnerObj->Id == $RT::Nobody->Id ) { unless ( $self->CurrentUserHasRight('ModifyTicket') || $self->CurrentUserHasRight('TakeTicket') ) { + $RT::Handle->Rollback(); return ( 0, $self->loc("Permission Denied") ); } } # see if it's a steal - elsif ( $self->OwnerObj->Id != $RT::Nobody->Id - && $self->OwnerObj->Id != $self->CurrentUser->id ) { + elsif ( $OldOwnerObj->Id != $RT::Nobody->Id + && $OldOwnerObj->Id != $self->CurrentUser->id ) { unless ( $self->CurrentUserHasRight('ModifyTicket') || $self->CurrentUserHasRight('StealTicket') ) { + $RT::Handle->Rollback(); return ( 0, $self->loc("Permission Denied") ); } } else { unless ( $self->CurrentUserHasRight('ModifyTicket') ) { + $RT::Handle->Rollback(); return ( 0, $self->loc("Permission Denied") ); } } - my $NewOwnerObj = RT::User->new( $self->CurrentUser ); - my $OldOwnerObj = $self->OwnerObj; - $NewOwnerObj->Load($NewOwner); - if ( !$NewOwnerObj->Id ) { - return ( 0, $self->loc("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, - $self->loc( -"You can only reassign tickets that you own or that are unowned" ) ); + # If we're not stealing and the ticket has an owner and it's not + # the current user + if ( $Type ne 'Steal' and $Type ne 'Force' + and $OldOwnerObj->Id != $RT::Nobody->Id + and $OldOwnerObj->Id != $self->CurrentUser->Id ) + { + $RT::Handle->Rollback(); + return ( 0, $self->loc("You can only take tickets that are unowned") ) + if $NewOwnerObj->id == $self->CurrentUser->id; + return ( + 0, + $self->loc("You can only reassign tickets that you own or that are unowned" ) + ); } #If we've specified a new owner and that user can't modify the ticket - elsif ( ( $NewOwnerObj->Id ) - and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', - Object => $self ) ) - ) { + elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) { + $RT::Handle->Rollback(); return ( 0, $self->loc("That user may not own tickets in that queue") ); } - #If the ticket has an owner and it's the new owner, we don't need - #To do anything - elsif ( ( $self->OwnerObj ) - and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) { + # If the ticket has an owner and it's the new owner, we don't need + # To do anything + elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) { + $RT::Handle->Rollback(); return ( 0, $self->loc("That user already owns that ticket") ); } - $RT::Handle->BeginTransaction(); - # Delete the owner in the owner group, then add a new one # TODO: is this safe? it's not how we really want the API to work # for most things, but it's fast. @@ -3025,8 +3183,6 @@ sub SetOwner { return ( 0, $self->loc("Could not change owner. ") . $msg ); } - $RT::Handle->Commit(); - ($val, $msg) = $self->_NewTransaction( Type => $Type, Field => 'Owner', @@ -3038,9 +3194,14 @@ sub SetOwner { if ( $val ) { $msg = $self->loc( "Owner changed from [_1] to [_2]", $OldOwnerObj->Name, $NewOwnerObj->Name ); - - # TODO: make sure the trans committed properly } + else { + $RT::Handle->Rollback(); + return ( 0, $msg ); + } + + $RT::Handle->Commit(); + return ( $val, $msg ); } @@ -3385,6 +3546,8 @@ sub DESTROY { return if $self->{_Destroyed}++; my $batch = $self->TransactionBatch or return; + return unless @$batch; + require RT::Scrips; RT::Scrips->new($RT::SystemUser)->Apply( Stage => 'TransactionBatch', @@ -3617,6 +3780,26 @@ sub HasRight { # }}} +=head2 Reminders + +Return the Reminders object for this ticket. (It's an RT::Reminders object.) +It isn't acutally a searchbuilder collection itself. + +=cut + +sub Reminders { + my $self = shift; + + unless ($self->{'__reminders'}) { + $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser); + $self->{'__reminders'}->Ticket($self->id); + } + return $self->{'__reminders'}; + +} + + + # {{{ sub Transactions =head2 Transactions @@ -3661,7 +3844,7 @@ sub Transactions { =head2 TransactionCustomFields - Returns the custom fields that transactions on tickets will ahve. + Returns the custom fields that transactions on tickets will have. =cut @@ -3696,7 +3879,6 @@ sub CustomFieldValues { # If we didn't find a valid cfid, give up. return RT::CustomFieldValues->new($self->CurrentUser); } - $field = $cf->id; } return $self->SUPER::CustomFieldValues($field); }