1# -- 2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 3# -- 4# This software comes with ABSOLUTELY NO WARRANTY. For details, see 5# the enclosed file COPYING for license information (GPL). If you 6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. 7# -- 8 9package Kernel::Modules::AgentTicketMerge; 10 11use strict; 12use warnings; 13 14use Kernel::System::VariableCheck qw(:all); 15use Kernel::Language qw(Translatable); 16use Mail::Address; 17 18our $ObjectManagerDisabled = 1; 19 20sub new { 21 my ( $Type, %Param ) = @_; 22 23 my $Self = {%Param}; 24 bless( $Self, $Type ); 25 26 return $Self; 27} 28 29sub Run { 30 my ( $Self, %Param ) = @_; 31 32 my $Output; 33 my %Error; 34 my %GetParam; 35 36 my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout'); 37 38 if ( !$Self->{TicketID} ) { 39 return $LayoutObject->ErrorScreen( 40 Message => Translatable('No TicketID is given!'), 41 Comment => Translatable('Please contact the administrator.'), 42 ); 43 } 44 45 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 46 my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); 47 48 # get config param 49 my $Config = $ConfigObject->Get("Ticket::Frontend::$Self->{Action}"); 50 51 # check permissions 52 my $Access = $TicketObject->TicketPermission( 53 Type => $Config->{Permission}, 54 TicketID => $Self->{TicketID}, 55 UserID => $Self->{UserID} 56 ); 57 58 # error screen, don't show ticket 59 if ( !$Access ) { 60 return $LayoutObject->NoPermission( WithHeader => 'yes' ); 61 } 62 63 # get ACL restrictions 64 my %PossibleActions = ( 1 => $Self->{Action} ); 65 66 my $ACL = $TicketObject->TicketAcl( 67 Data => \%PossibleActions, 68 Action => $Self->{Action}, 69 TicketID => $Self->{TicketID}, 70 ReturnType => 'Action', 71 ReturnSubType => '-', 72 UserID => $Self->{UserID}, 73 ); 74 my %AclAction = $TicketObject->TicketAclActionData(); 75 76 # check if ACL restrictions exist 77 if ( $ACL || IsHashRefWithData( \%AclAction ) ) { 78 79 my %AclActionLookup = reverse %AclAction; 80 81 # show error screen if ACL prohibits this action 82 if ( !$AclActionLookup{ $Self->{Action} } ) { 83 return $LayoutObject->NoPermission( WithHeader => 'yes' ); 84 } 85 } 86 87 # get ticket data 88 my %Ticket = $TicketObject->TicketGet( TicketID => $Self->{TicketID} ); 89 90 # get lock state && write (lock) permissions 91 if ( $Config->{RequiredLock} ) { 92 if ( !$TicketObject->TicketLockGet( TicketID => $Self->{TicketID} ) ) { 93 94 my $Lock = $TicketObject->TicketLockSet( 95 TicketID => $Self->{TicketID}, 96 Lock => 'lock', 97 UserID => $Self->{UserID} 98 ); 99 100 # Set new owner if ticket owner is different then logged user. 101 if ( $Lock && ( $Ticket{OwnerID} != $Self->{UserID} ) ) { 102 103 # Remember previous owner, which will be used to restore ticket owner on undo action. 104 $Param{PreviousOwner} = $Ticket{OwnerID}; 105 106 my $Success = $TicketObject->TicketOwnerSet( 107 TicketID => $Self->{TicketID}, 108 UserID => $Self->{UserID}, 109 NewUserID => $Self->{UserID}, 110 ); 111 112 # Show lock state. 113 if ($Success) { 114 $LayoutObject->Block( 115 Name => 'PropertiesLock', 116 Data => { 117 %Param, 118 TicketID => $Self->{TicketID} 119 }, 120 ); 121 } 122 } 123 } 124 else { 125 my $AccessOk = $TicketObject->OwnerCheck( 126 TicketID => $Self->{TicketID}, 127 OwnerID => $Self->{UserID}, 128 ); 129 if ( !$AccessOk ) { 130 my $Output = $LayoutObject->Header( 131 Value => $Ticket{Number}, 132 Type => 'Small', 133 BodyClass => 'Popup', 134 ); 135 $Output .= $LayoutObject->Warning( 136 Message => Translatable('Sorry, you need to be the ticket owner to perform this action.'), 137 Comment => Translatable('Please change the owner first.'), 138 ); 139 $Output .= $LayoutObject->Footer( 140 Type => 'Small', 141 ); 142 return $Output; 143 } 144 145 # show back link 146 $LayoutObject->Block( 147 Name => 'TicketBack', 148 Data => { %Param, TicketID => $Self->{TicketID} }, 149 ); 150 } 151 } 152 else { 153 154 # show back link 155 $LayoutObject->Block( 156 Name => 'TicketBack', 157 Data => { %Param, TicketID => $Self->{TicketID} }, 158 ); 159 } 160 161 # merge action 162 if ( $Self->{Subaction} eq 'Merge' ) { 163 164 # challenge token check for write action 165 $LayoutObject->ChallengeTokenCheck(); 166 167 # get all parameters 168 for my $Parameter (qw( From To Subject Body InformSender MainTicketNumber )) { 169 $GetParam{$Parameter} = $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => $Parameter ) 170 || ''; 171 } 172 173 # rewrap body if no rich text is used 174 if ( $GetParam{Body} && !$LayoutObject->{BrowserRichText} ) { 175 $GetParam{Body} = $LayoutObject->WrapPlainText( 176 MaxCharacters => $ConfigObject->Get('Ticket::Frontend::TextAreaNote'), 177 PlainText => $GetParam{Body}, 178 ); 179 } 180 181 # get check item object 182 my $CheckItemObject = $Kernel::OM->Get('Kernel::System::CheckItem'); 183 184 # removing blank spaces from the ticket number 185 $CheckItemObject->StringClean( 186 StringRef => \$GetParam{'MainTicketNumber'}, 187 TrimLeft => 1, 188 TrimRight => 1, 189 ); 190 191 # check some stuff 192 my $MainTicketID = $TicketObject->TicketIDLookup( 193 TicketNumber => $GetParam{'MainTicketNumber'}, 194 ); 195 196 # check if source and target TicketID are the same (bug#8667) 197 if ( $MainTicketID && $MainTicketID == $Self->{TicketID} ) { 198 $LayoutObject->FatalError( 199 Message => Translatable('Can\'t merge ticket with itself!'), 200 ); 201 } 202 203 # check for errors 204 if ( !$MainTicketID ) { 205 $Error{'MainTicketNumberInvalid'} = 'ServerError'; 206 } 207 208 if ( $GetParam{InformSender} ) { 209 for my $Parameter (qw( To Subject Body )) { 210 if ( !$GetParam{$Parameter} ) { 211 $Error{ $Parameter . 'Invalid' } = 'ServerError'; 212 } 213 } 214 215 # check forward email address(es) 216 if ( $GetParam{To} ) { 217 for my $Email ( Mail::Address->parse( $GetParam{To} ) ) { 218 my $Address = $Email->address(); 219 if ( 220 $Kernel::OM->Get('Kernel::System::SystemAddress') 221 ->SystemAddressIsLocalAddress( Address => $Address ) 222 ) 223 { 224 $LayoutObject->Block( Name => 'ToCustomerGenericServerErrorMsg' ); 225 $Error{'ToInvalid'} = 'ServerError'; 226 } 227 228 # check email address 229 elsif ( !$CheckItemObject->CheckEmail( Address => $Address ) ) { 230 my $ToErrorMsg = 231 'To' 232 . $CheckItemObject->CheckErrorType() 233 . 'ServerErrorMsg'; 234 $LayoutObject->Block( Name => $ToErrorMsg ); 235 $Error{'ToInvalid'} = 'ServerError'; 236 } 237 } 238 } 239 else { 240 $LayoutObject->Block( Name => 'ToCustomerGenericServerErrorMsg' ); 241 } 242 } 243 244 if (%Error) { 245 my $Output = $LayoutObject->Header( 246 Type => 'Small', 247 BodyClass => 'Popup', 248 ); 249 250 # add rich text editor 251 if ( $LayoutObject->{BrowserRichText} ) { 252 253 # use height/width defined for this screen 254 $Param{RichTextHeight} = $Config->{RichTextHeight} || 0; 255 $Param{RichTextWidth} = $Config->{RichTextWidth} || 0; 256 257 # set up rich text editor 258 $LayoutObject->SetRichTextParameters( 259 Data => \%Param, 260 ); 261 262 } 263 264 $Param{InformSenderChecked} = $GetParam{InformSender} ? 'checked="checked"' : ''; 265 266 $Output .= $LayoutObject->Output( 267 TemplateFile => 'AgentTicketMerge', 268 Data => { %Param, %GetParam, %Ticket, %Error }, 269 ); 270 $Output .= $LayoutObject->Footer( 271 Type => 'Small', 272 ); 273 return $Output; 274 } 275 276 # challenge token check for write action 277 $LayoutObject->ChallengeTokenCheck(); 278 279 # check permissions 280 my $Access = $TicketObject->TicketPermission( 281 Type => $Config->{Permission}, 282 TicketID => $MainTicketID, 283 UserID => $Self->{UserID}, 284 ); 285 286 # error screen, don't show ticket 287 if ( !$Access ) { 288 return $LayoutObject->NoPermission( WithHeader => 'yes' ); 289 } 290 291 my $TicketMergeResult = $TicketObject->TicketMerge( 292 MainTicketID => $MainTicketID, 293 MergeTicketID => $Self->{TicketID}, 294 UserID => $Self->{UserID}, 295 ); 296 297 # check errors 298 if ( 299 $Self->{TicketID} == $MainTicketID 300 || !$TicketMergeResult 301 || $TicketMergeResult eq 'NoValidMergeStates' 302 ) 303 { 304 my $Output = $LayoutObject->Header( 305 Type => 'Small', 306 BodyClass => 'Popup', 307 ); 308 309 if ( $TicketMergeResult eq 'NoValidMergeStates' ) { 310 $Output .= $LayoutObject->Notify( 311 Priority => 'Error', 312 Info => 'No merge state found! Please add a valid merge state before merge action.', 313 ); 314 } 315 316 # add rich text editor 317 if ( $LayoutObject->{BrowserRichText} ) { 318 319 # use height/width defined for this screen 320 $Param{RichTextHeight} = $Config->{RichTextHeight} || 0; 321 $Param{RichTextWidth} = $Config->{RichTextWidth} || 0; 322 323 # set up rich text editor 324 $LayoutObject->SetRichTextParameters( 325 Data => \%Param, 326 ); 327 } 328 329 $Output .= $LayoutObject->Output( 330 TemplateFile => 'AgentTicketMerge', 331 Data => { %Param, %Ticket }, 332 ); 333 $Output .= $LayoutObject->Footer( 334 Type => 'Small', 335 ); 336 return $Output; 337 } 338 else { 339 340 # send customer info? 341 if ( $GetParam{InformSender} ) { 342 my $MimeType = 'text/plain'; 343 if ( $LayoutObject->{BrowserRichText} ) { 344 $MimeType = 'text/html'; 345 346 # verify html document 347 $GetParam{Body} = $LayoutObject->RichTextDocumentComplete( 348 String => $GetParam{Body}, 349 ); 350 } 351 my %Ticket = $TicketObject->TicketGet( TicketID => $Self->{TicketID} ); 352 $GetParam{Body} =~ s/(<|<)OTRS_TICKET(>|>)/$Ticket{TicketNumber}/g; 353 $GetParam{Body} 354 =~ s/(<|<)OTRS_MERGE_TO_TICKET(>|>)/$GetParam{'MainTicketNumber'}/g; 355 356 my $EmailArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForChannel( 357 ChannelName => 'Email', 358 ); 359 360 my $ArticleID = $EmailArticleBackendObject->ArticleSend( 361 TicketID => $Self->{TicketID}, 362 SenderType => 'agent', 363 IsVisibleForCustomer => 1, 364 HistoryType => 'SendAnswer', 365 HistoryComment => "Merge info to '$GetParam{To}'.", 366 From => $GetParam{From}, 367 Email => $GetParam{Email}, 368 To => $GetParam{To}, 369 Subject => $GetParam{Subject}, 370 UserID => $Self->{UserID}, 371 Body => $GetParam{Body}, 372 Charset => $LayoutObject->{UserCharset}, 373 MimeType => $MimeType, 374 ); 375 if ( !$ArticleID ) { 376 377 # error page 378 return $LayoutObject->ErrorScreen(); 379 } 380 } 381 382 # redirect to merged ticket 383 return $LayoutObject->PopupClose( 384 URL => "Action=AgentTicketZoom;TicketID=$MainTicketID", 385 ); 386 } 387 } 388 else { 389 my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); 390 391 # Get last customer article. 392 my @Articles = $ArticleObject->ArticleList( 393 TicketID => $Self->{TicketID}, 394 SenderType => 'customer', 395 OnlyLast => 1, 396 ); 397 398 # If the ticket has no customer article, get the last agent article. 399 if ( !@Articles ) { 400 @Articles = $ArticleObject->ArticleList( 401 TicketID => $Self->{TicketID}, 402 SenderType => 'agent', 403 OnlyLast => 1, 404 ); 405 } 406 407 # Finally, if everything failed, get latest article. 408 if ( !@Articles ) { 409 @Articles = $ArticleObject->ArticleList( 410 TicketID => $Self->{TicketID}, 411 OnlyLast => 1, 412 ); 413 } 414 415 my %Article; 416 for my $Article (@Articles) { 417 %Article = $ArticleObject->BackendForArticle( %{$Article} )->ArticleGet( 418 %{$Article}, 419 DynamicFields => 1, 420 ); 421 } 422 423 # merge box 424 my $Output = $LayoutObject->Header( 425 Value => $Ticket{TicketNumber}, 426 Type => 'Small', 427 BodyClass => 'Popup', 428 ); 429 430 # prepare salutation 431 my $TemplateGenerator = $Kernel::OM->Get('Kernel::System::TemplateGenerator'); 432 my $Salutation = $TemplateGenerator->Salutation( 433 TicketID => $Self->{TicketID}, 434 ArticleID => $Article{ArticleID}, 435 Data => {%Article}, 436 UserID => $Self->{UserID}, 437 ); 438 439 # prepare signature 440 my $Signature = $TemplateGenerator->Signature( 441 TicketID => $Self->{TicketID}, 442 ArticleID => $Article{ArticleID}, 443 Data => {%Article}, 444 UserID => $Self->{UserID}, 445 ); 446 447 # prepare subject ... 448 $Article{Subject} = $TicketObject->TicketSubjectBuild( 449 TicketNumber => $Ticket{TicketNumber}, 450 Subject => $Article{Subject} || '', 451 ); 452 453 # prepare from ... 454 $Article{To} = $Article{From}; 455 my %Address = $Kernel::OM->Get('Kernel::System::Queue')->GetSystemAddress( QueueID => $Ticket{QueueID} ); 456 $Article{From} = "$Address{RealName} <$Address{Email}>"; 457 458 # add salutation and signature to body 459 if ( $LayoutObject->{BrowserRichText} ) { 460 my $Body = $LayoutObject->Ascii2RichText( 461 String => $ConfigObject->Get('Ticket::Frontend::MergeText'), 462 ); 463 $Article{Body} = $Salutation 464 . '<br/><br/>' 465 . $Body 466 . '<br/><br/>' 467 . $Signature; 468 } 469 else { 470 $Article{Body} = $Salutation 471 . "\n\n" 472 . $ConfigObject->Get('Ticket::Frontend::MergeText') 473 . "\n\n" 474 . $Signature; 475 } 476 477 # add rich text editor 478 if ( $LayoutObject->{BrowserRichText} ) { 479 480 # use height/width defined for this screen 481 $Param{RichTextHeight} = $Config->{RichTextHeight} || 0; 482 $Param{RichTextWidth} = $Config->{RichTextWidth} || 0; 483 484 # set up rich text editor 485 $LayoutObject->SetRichTextParameters( 486 Data => \%Param, 487 ); 488 } 489 490 $Output .= $LayoutObject->Output( 491 TemplateFile => 'AgentTicketMerge', 492 Data => { %Param, %Ticket, %Article, } 493 ); 494 $Output .= $LayoutObject->Footer( 495 Type => 'Small', 496 ); 497 return $Output; 498 } 499} 500 5011; 502