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::Output::HTML::Layout::Article; 10 11use strict; 12use warnings; 13 14our $ObjectManagerDisabled = 1; 15 16=head1 NAME 17 18Kernel::Output::HTML::Layout::Article - Helper functions for article rendering. 19 20=head1 PUBLIC INTERFACE 21 22=head2 ArticleFields() 23 24Get article fields as returned by specific article backend. 25 26 my %ArticleFields = $LayoutObject->ArticleFields( 27 TicketID => 123, # (required) 28 ArticleID => 123, # (required) 29 ); 30 31Returns article fields hash: 32 33 %ArticleFields = ( 34 Sender => { # mandatory 35 Label => 'Sender', 36 Value => 'John Smith', 37 Prio => 100, 38 }, 39 Subject => { # mandatory 40 Label => 'Subject', 41 Value => 'Message', 42 Prio => 200, 43 }, 44 ... 45 ); 46 47=cut 48 49sub ArticleFields { 50 my ( $Self, %Param ) = @_; 51 52 # Check needed stuff. 53 for my $Needed (qw(TicketID ArticleID)) { 54 if ( !$Param{$Needed} ) { 55 $Kernel::OM->Get('Kernel::System::Log')->Log( 56 Priority => 'error', 57 Message => "Need $Needed!", 58 ); 59 return; 60 } 61 } 62 63 my $BackendObject = $Self->_BackendGet(%Param); 64 65 # Return backend response. 66 return $BackendObject->ArticleFields( 67 %Param, 68 ); 69} 70 71=head2 ArticlePreview() 72 73Get article content preview as returned by specific article backend. 74 75 my $ArticlePreview = $LayoutObject->ArticlePreview( 76 TicketID => 123, # (required) 77 ArticleID => 123, # (required) 78 ResultType => 'plain', # (optional) plain|HTML, default: HTML 79 MaxLength => 50, # (optional) performs trimming (for plain result only) 80 ); 81 82Returns article preview in scalar form: 83 84 $ArticlePreview = 'Hello, world!'; 85 86=cut 87 88sub ArticlePreview { 89 my ( $Self, %Param ) = @_; 90 91 # Check needed stuff. 92 for my $Needed (qw(TicketID ArticleID)) { 93 if ( !$Param{$Needed} ) { 94 $Kernel::OM->Get('Kernel::System::Log')->Log( 95 Priority => 'error', 96 Message => "Need $Needed!", 97 ); 98 return; 99 } 100 } 101 102 my $BackendObject = $Self->_BackendGet(%Param); 103 104 # Return backend response. 105 return $BackendObject->ArticlePreview( 106 %Param, 107 ); 108} 109 110=head2 ArticleActions() 111 112Get available article actions as returned by specific article backend. 113 114 my @Actions = $LayoutObject->ArticleActions( 115 TicketID => 123, # (required) 116 ArticleID => 123, # (required) 117 ); 118 119Returns article action array: 120 121 @Actions = ( 122 { 123 ItemType => 'Dropdown', 124 DropdownType => 'Reply', 125 StandardResponsesStrg => $StandardResponsesStrg, 126 Name => 'Reply', 127 Class => 'AsPopup PopupType_TicketAction', 128 Action => 'AgentTicketCompose', 129 FormID => 'Reply' . $Article{ArticleID}, 130 ResponseElementID => 'ResponseID', 131 Type => $Param{Type}, 132 }, 133 { 134 ItemType => 'Link', 135 Description => 'Forward article via mail', 136 Name => 'Forward', 137 Class => 'AsPopup PopupType_TicketAction', 138 Link => 139 "Action=AgentTicketForward;TicketID=$Ticket{TicketID};ArticleID=$Article{ArticleID}" 140 }, 141 ... 142 ); 143 144=cut 145 146sub ArticleActions { 147 my ( $Self, %Param ) = @_; 148 149 # Check needed stuff. 150 for my $Needed (qw(TicketID ArticleID)) { 151 if ( !$Param{$Needed} ) { 152 $Kernel::OM->Get('Kernel::System::Log')->Log( 153 Priority => 'error', 154 Message => "Need $Needed!", 155 ); 156 return; 157 } 158 } 159 160 my $BackendObject = $Self->_BackendGet(%Param); 161 162 # Return backend response. 163 return $BackendObject->ArticleActions( 164 %Param, 165 UserID => $Self->{UserID}, 166 ); 167} 168 169=head2 ArticleCustomerRecipientsGet() 170 171Get customer users from an article to use as recipients. 172 173 my @CustomerUserIDs = $LayoutObject->ArticleCustomerRecipientsGet( 174 TicketID => 123, # (required) 175 ArticleID => 123, # (required) 176 ); 177 178Returns array of customer user IDs who should receive a message: 179 180 @CustomerUserIDs = ( 181 'customer-1', 182 'customer-2', 183 ... 184 ); 185 186=cut 187 188sub ArticleCustomerRecipientsGet { 189 my ( $Self, %Param ) = @_; 190 191 for my $Needed (qw(TicketID ArticleID)) { 192 if ( !$Param{$Needed} ) { 193 $Kernel::OM->Get('Kernel::System::Log')->Log( 194 Priority => 'error', 195 Message => "Need $Needed!", 196 ); 197 return; 198 } 199 } 200 201 my $BackendObject = $Self->_BackendGet(%Param); 202 203 # Return backend response. 204 return $BackendObject->ArticleCustomerRecipientsGet( 205 %Param, 206 UserID => $Self->{UserID}, 207 ); 208} 209 210=head2 ArticleQuote() 211 212get body and attach e. g. inline documents and/or attach all attachments to 213upload cache 214 215for forward or split, get body and attach all attachments 216 217 my $HTMLBody = $LayoutObject->ArticleQuote( 218 TicketID => 123, 219 ArticleID => 123, 220 FormID => $Self->{FormID}, 221 UploadCacheObject => $Self->{UploadCacheObject}, 222 AttachmentsInclude => 1, 223 ); 224 225or just for including inline documents to upload cache 226 227 my $HTMLBody = $LayoutObject->ArticleQuote( 228 TicketID => 123, 229 ArticleID => 123, 230 FormID => $Self->{FormID}, 231 UploadCacheObject => $Self->{UploadCacheObject}, 232 AttachmentsInclude => 0, 233 ); 234 235Both will also work without rich text (if $ConfigObject->Get('Frontend::RichText') 236is false), return param will be text/plain instead. 237 238=cut 239 240sub ArticleQuote { 241 my ( $Self, %Param ) = @_; 242 243 for my $Needed (qw(TicketID ArticleID FormID UploadCacheObject)) { 244 if ( !$Param{$Needed} ) { 245 $Self->FatalError( Message => "Need $Needed!" ); 246 } 247 } 248 249 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 250 my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); 251 my $ArticleBackendObject = $ArticleObject->BackendForArticle( 252 ArticleID => $Param{ArticleID}, 253 TicketID => $Param{TicketID} 254 ); 255 256 # body preparation for plain text processing 257 if ( $ConfigObject->Get('Frontend::RichText') ) { 258 259 my $Body = ''; 260 261 my %NotInlineAttachments; 262 263 my %QuoteArticle = $ArticleBackendObject->ArticleGet( 264 TicketID => $Param{TicketID}, 265 ArticleID => $Param{ArticleID}, 266 DynamicFields => 0, 267 ); 268 269 # Get the attachments without message bodies. 270 $QuoteArticle{Atms} = { 271 $ArticleBackendObject->ArticleAttachmentIndex( 272 ArticleID => $Param{ArticleID}, 273 ExcludePlainText => 1, 274 ExcludeHTMLBody => 1, 275 ) 276 }; 277 278 # Check if there is HTML body attachment. 279 my %AttachmentIndexHTMLBody = $ArticleBackendObject->ArticleAttachmentIndex( 280 ArticleID => $Param{ArticleID}, 281 OnlyHTMLBody => 1, 282 ); 283 my ($HTMLBodyAttachmentID) = sort keys %AttachmentIndexHTMLBody; 284 285 if ($HTMLBodyAttachmentID) { 286 my %AttachmentHTML = $ArticleBackendObject->ArticleAttachment( 287 TicketID => $QuoteArticle{TicketID}, 288 ArticleID => $QuoteArticle{ArticleID}, 289 FileID => $HTMLBodyAttachmentID, 290 ); 291 my $Charset = $AttachmentHTML{ContentType} || ''; 292 $Charset =~ s/.+?charset=("|'|)(\w+)/$2/gi; 293 $Charset =~ s/"|'//g; 294 $Charset =~ s/(.+?);.*/$1/g; 295 296 # convert html body to correct charset 297 $Body = $Kernel::OM->Get('Kernel::System::Encode')->Convert( 298 Text => $AttachmentHTML{Content}, 299 From => $Charset, 300 To => $Self->{UserCharset}, 301 Check => 1, 302 ); 303 304 # get HTML utils object 305 my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils'); 306 307 # add url quoting 308 $Body = $HTMLUtilsObject->LinkQuote( 309 String => $Body, 310 ); 311 312 # strip head, body and meta elements 313 $Body = $HTMLUtilsObject->DocumentStrip( 314 String => $Body, 315 ); 316 317 # display inline images if exists 318 my $SessionID = ''; 319 if ( $Self->{SessionID} && !$Self->{SessionIDCookie} ) { 320 $SessionID = ';' . $Self->{SessionName} . '=' . $Self->{SessionID}; 321 } 322 my $AttachmentLink = $Self->{Baselink} 323 . 'Action=PictureUpload' 324 . ';FormID=' 325 . $Param{FormID} 326 . $SessionID 327 . ';ContentID='; 328 329 # search inline documents in body and add it to upload cache 330 my %Attachments = %{ $QuoteArticle{Atms} }; 331 my %AttachmentAlreadyUsed; 332 $Body =~ s{ 333 (=|"|')cid:(.*?)("|'|>|\/>|\s) 334 } 335 { 336 my $Start= $1; 337 my $ContentID = $2; 338 my $End = $3; 339 340 # improve html quality 341 if ( $Start ne '"' && $Start ne '\'' ) { 342 $Start .= '"'; 343 } 344 if ( $End ne '"' && $End ne '\'' ) { 345 $End = '"' . $End; 346 } 347 348 # find attachment to include 349 ATMCOUNT: 350 for my $AttachmentID ( sort keys %Attachments ) { 351 352 if ( lc $Attachments{$AttachmentID}->{ContentID} ne lc "<$ContentID>" ) { 353 next ATMCOUNT; 354 } 355 356 # get whole attachment 357 my %AttachmentPicture = $ArticleBackendObject->ArticleAttachment( 358 TicketID => $Param{TicketID}, 359 ArticleID => $Param{ArticleID}, 360 FileID => $AttachmentID, 361 ); 362 363 # content id cleanup 364 $AttachmentPicture{ContentID} =~ s/^<//; 365 $AttachmentPicture{ContentID} =~ s/>$//; 366 367 # find cid, add attachment URL and remember, file is already uploaded 368 $ContentID = $AttachmentLink . $Self->LinkEncode( $AttachmentPicture{ContentID} ); 369 370 # add to upload cache if not uploaded and remember 371 if (!$AttachmentAlreadyUsed{$AttachmentID}) { 372 373 # remember 374 $AttachmentAlreadyUsed{$AttachmentID} = 1; 375 376 # write attachment to upload cache 377 $Param{UploadCacheObject}->FormIDAddFile( 378 FormID => $Param{FormID}, 379 Disposition => 'inline', 380 %{ $Attachments{$AttachmentID} }, 381 %AttachmentPicture, 382 ); 383 } 384 } 385 386 # return link 387 $Start . $ContentID . $End; 388 }egxi; 389 390 # find inline images using Content-Location instead of Content-ID 391 ATTACHMENT: 392 for my $AttachmentID ( sort keys %Attachments ) { 393 394 next ATTACHMENT if !$Attachments{$AttachmentID}->{ContentID}; 395 396 # get whole attachment 397 my %AttachmentPicture = $ArticleBackendObject->ArticleAttachment( 398 TicketID => $Param{TicketID}, 399 ArticleID => $Param{ArticleID}, 400 FileID => $AttachmentID, 401 ); 402 403 # content id cleanup 404 $AttachmentPicture{ContentID} =~ s/^<//; 405 $AttachmentPicture{ContentID} =~ s/>$//; 406 407 $Body =~ s{ 408 ("|')(\Q$AttachmentPicture{ContentID}\E)("|'|>|\/>|\s) 409 } 410 { 411 my $Start= $1; 412 my $ContentID = $2; 413 my $End = $3; 414 415 # find cid, add attachment URL and remember, file is already uploaded 416 $ContentID = $AttachmentLink . $Self->LinkEncode( $AttachmentPicture{ContentID} ); 417 418 # add to upload cache if not uploaded and remember 419 if (!$AttachmentAlreadyUsed{$AttachmentID}) { 420 421 # remember 422 $AttachmentAlreadyUsed{$AttachmentID} = 1; 423 424 # write attachment to upload cache 425 $Param{UploadCacheObject}->FormIDAddFile( 426 FormID => $Param{FormID}, 427 Disposition => 'inline', 428 %{ $Attachments{$AttachmentID} }, 429 %AttachmentPicture, 430 ); 431 } 432 433 # return link 434 $Start . $ContentID . $End; 435 }egxi; 436 } 437 438 # find not inline images 439 ATTACHMENT: 440 for my $AttachmentID ( sort keys %Attachments ) { 441 next ATTACHMENT if $AttachmentAlreadyUsed{$AttachmentID}; 442 $NotInlineAttachments{$AttachmentID} = 1; 443 } 444 } 445 446 # attach also other attachments on article forward 447 if ( $Body && $Param{AttachmentsInclude} ) { 448 for my $AttachmentID ( sort keys %NotInlineAttachments ) { 449 my %Attachment = $ArticleBackendObject->ArticleAttachment( 450 TicketID => $Param{TicketID}, 451 ArticleID => $Param{ArticleID}, 452 FileID => $AttachmentID, 453 ); 454 455 # add attachment 456 $Param{UploadCacheObject}->FormIDAddFile( 457 FormID => $Param{FormID}, 458 %Attachment, 459 Disposition => 'attachment', 460 ); 461 } 462 } 463 464 # Fallback for non-MIMEBase articles: get article HTML content if it exists. 465 if ( !$Body ) { 466 $Body = $Self->ArticlePreview( 467 TicketID => $Param{TicketID}, 468 ArticleID => $Param{ArticleID}, 469 ); 470 } 471 472 return $Body if $Body; 473 } 474 475 # as fallback use text body for quote 476 my %Article = $ArticleBackendObject->ArticleGet( 477 TicketID => $Param{TicketID}, 478 ArticleID => $Param{ArticleID}, 479 DynamicFields => 0, 480 ); 481 482 # check if original content isn't text/plain or text/html, don't use it 483 if ( !$Article{ContentType} ) { 484 $Article{ContentType} = 'text/plain'; 485 } 486 487 if ( $Article{ContentType} !~ /text\/(plain|html)/i ) { 488 $Article{Body} = '-> no quotable message <-'; 489 $Article{ContentType} = 'text/plain'; 490 } 491 else { 492 $Article{Body} = $Self->WrapPlainText( 493 MaxCharacters => $ConfigObject->Get('Ticket::Frontend::TextAreaEmail') || 82, 494 PlainText => $Article{Body}, 495 ); 496 } 497 498 # attach attachments 499 if ( $Param{AttachmentsInclude} ) { 500 my %ArticleIndex = $ArticleBackendObject->ArticleAttachmentIndex( 501 ArticleID => $Param{ArticleID}, 502 ExcludePlainText => 1, 503 ExcludeHTMLBody => 1, 504 ); 505 for my $Index ( sort keys %ArticleIndex ) { 506 my %Attachment = $ArticleBackendObject->ArticleAttachment( 507 TicketID => $Param{TicketID}, 508 ArticleID => $Param{ArticleID}, 509 FileID => $Index, 510 ); 511 512 # add attachment 513 $Param{UploadCacheObject}->FormIDAddFile( 514 FormID => $Param{FormID}, 515 %Attachment, 516 Disposition => 'attachment', 517 ); 518 } 519 } 520 521 # return body as html 522 if ( $ConfigObject->Get('Frontend::RichText') ) { 523 524 $Article{Body} = $Self->Ascii2Html( 525 Text => $Article{Body}, 526 HTMLResultMode => 1, 527 LinkFeature => 1, 528 ); 529 } 530 531 # return body as plain text 532 return $Article{Body}; 533} 534 535sub _BackendGet { 536 my ( $Self, %Param ) = @_; 537 538 # Check needed stuff. 539 for my $Needed (qw(TicketID ArticleID)) { 540 if ( !$Param{$Needed} ) { 541 $Kernel::OM->Get('Kernel::System::Log')->Log( 542 Priority => 'error', 543 Message => "Need $Needed!", 544 ); 545 return; 546 } 547 } 548 549 my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForArticle(%Param); 550 551 # Determine channel name for this article. 552 my $ChannelName = $ArticleBackendObject->ChannelNameGet(); 553 554 my $Loaded = $Kernel::OM->Get('Kernel::System::Main')->Require( 555 "Kernel::Output::HTML::Article::$ChannelName", 556 ); 557 return if !$Loaded; 558 559 return $Kernel::OM->Get("Kernel::Output::HTML::Article::$ChannelName"); 560} 561 5621; 563 564=head1 TERMS AND CONDITIONS 565 566This software is part of the OTRS project (L<https://otrs.org/>). 567 568This software comes with ABSOLUTELY NO WARRANTY. For details, see 569the enclosed file COPYING for license information (GPL). If you 570did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 571 572=cut 573