# -- # Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::Output::HTML::Layout::Article; use strict; use warnings; our $ObjectManagerDisabled = 1; =head1 NAME Kernel::Output::HTML::Layout::Article - Helper functions for article rendering. =head1 PUBLIC INTERFACE =head2 ArticleFields() Get article fields as returned by specific article backend. my %ArticleFields = $LayoutObject->ArticleFields( TicketID => 123, # (required) ArticleID => 123, # (required) ); Returns article fields hash: %ArticleFields = ( Sender => { # mandatory Label => 'Sender', Value => 'John Smith', Prio => 100, }, Subject => { # mandatory Label => 'Subject', Value => 'Message', Prio => 200, }, ... ); =cut sub ArticleFields { my ( $Self, %Param ) = @_; # Check needed stuff. for my $Needed (qw(TicketID ArticleID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $BackendObject = $Self->_BackendGet(%Param); # Return backend response. return $BackendObject->ArticleFields( %Param, ); } =head2 ArticlePreview() Get article content preview as returned by specific article backend. my $ArticlePreview = $LayoutObject->ArticlePreview( TicketID => 123, # (required) ArticleID => 123, # (required) ResultType => 'plain', # (optional) plain|HTML, default: HTML MaxLength => 50, # (optional) performs trimming (for plain result only) ); Returns article preview in scalar form: $ArticlePreview = 'Hello, world!'; =cut sub ArticlePreview { my ( $Self, %Param ) = @_; # Check needed stuff. for my $Needed (qw(TicketID ArticleID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $BackendObject = $Self->_BackendGet(%Param); # Return backend response. return $BackendObject->ArticlePreview( %Param, ); } =head2 ArticleActions() Get available article actions as returned by specific article backend. my @Actions = $LayoutObject->ArticleActions( TicketID => 123, # (required) ArticleID => 123, # (required) ); Returns article action array: @Actions = ( { ItemType => 'Dropdown', DropdownType => 'Reply', StandardResponsesStrg => $StandardResponsesStrg, Name => 'Reply', Class => 'AsPopup PopupType_TicketAction', Action => 'AgentTicketCompose', FormID => 'Reply' . $Article{ArticleID}, ResponseElementID => 'ResponseID', Type => $Param{Type}, }, { ItemType => 'Link', Description => 'Forward article via mail', Name => 'Forward', Class => 'AsPopup PopupType_TicketAction', Link => "Action=AgentTicketForward;TicketID=$Ticket{TicketID};ArticleID=$Article{ArticleID}" }, ... ); =cut sub ArticleActions { my ( $Self, %Param ) = @_; # Check needed stuff. for my $Needed (qw(TicketID ArticleID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $BackendObject = $Self->_BackendGet(%Param); # Return backend response. return $BackendObject->ArticleActions( %Param, UserID => $Self->{UserID}, ); } =head2 ArticleCustomerRecipientsGet() Get customer users from an article to use as recipients. my @CustomerUserIDs = $LayoutObject->ArticleCustomerRecipientsGet( TicketID => 123, # (required) ArticleID => 123, # (required) ); Returns array of customer user IDs who should receive a message: @CustomerUserIDs = ( 'customer-1', 'customer-2', ... ); =cut sub ArticleCustomerRecipientsGet { my ( $Self, %Param ) = @_; for my $Needed (qw(TicketID ArticleID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $BackendObject = $Self->_BackendGet(%Param); # Return backend response. return $BackendObject->ArticleCustomerRecipientsGet( %Param, UserID => $Self->{UserID}, ); } =head2 ArticleQuote() get body and attach e. g. inline documents and/or attach all attachments to upload cache for forward or split, get body and attach all attachments my $HTMLBody = $LayoutObject->ArticleQuote( TicketID => 123, ArticleID => 123, FormID => $Self->{FormID}, UploadCacheObject => $Self->{UploadCacheObject}, AttachmentsInclude => 1, ); or just for including inline documents to upload cache my $HTMLBody = $LayoutObject->ArticleQuote( TicketID => 123, ArticleID => 123, FormID => $Self->{FormID}, UploadCacheObject => $Self->{UploadCacheObject}, AttachmentsInclude => 0, ); Both will also work without rich text (if $ConfigObject->Get('Frontend::RichText') is false), return param will be text/plain instead. =cut sub ArticleQuote { my ( $Self, %Param ) = @_; for my $Needed (qw(TicketID ArticleID FormID UploadCacheObject)) { if ( !$Param{$Needed} ) { $Self->FatalError( Message => "Need $Needed!" ); } } my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); my $ArticleBackendObject = $ArticleObject->BackendForArticle( ArticleID => $Param{ArticleID}, TicketID => $Param{TicketID} ); # body preparation for plain text processing if ( $ConfigObject->Get('Frontend::RichText') ) { my $Body = ''; my %NotInlineAttachments; my %QuoteArticle = $ArticleBackendObject->ArticleGet( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, DynamicFields => 0, ); # Get the attachments without message bodies. $QuoteArticle{Atms} = { $ArticleBackendObject->ArticleAttachmentIndex( ArticleID => $Param{ArticleID}, ExcludePlainText => 1, ExcludeHTMLBody => 1, ) }; # Check if there is HTML body attachment. my %AttachmentIndexHTMLBody = $ArticleBackendObject->ArticleAttachmentIndex( ArticleID => $Param{ArticleID}, OnlyHTMLBody => 1, ); my ($HTMLBodyAttachmentID) = sort keys %AttachmentIndexHTMLBody; if ($HTMLBodyAttachmentID) { my %AttachmentHTML = $ArticleBackendObject->ArticleAttachment( TicketID => $QuoteArticle{TicketID}, ArticleID => $QuoteArticle{ArticleID}, FileID => $HTMLBodyAttachmentID, ); my $Charset = $AttachmentHTML{ContentType} || ''; $Charset =~ s/.+?charset=("|'|)(\w+)/$2/gi; $Charset =~ s/"|'//g; $Charset =~ s/(.+?);.*/$1/g; # convert html body to correct charset $Body = $Kernel::OM->Get('Kernel::System::Encode')->Convert( Text => $AttachmentHTML{Content}, From => $Charset, To => $Self->{UserCharset}, Check => 1, ); # get HTML utils object my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils'); # add url quoting $Body = $HTMLUtilsObject->LinkQuote( String => $Body, ); # strip head, body and meta elements $Body = $HTMLUtilsObject->DocumentStrip( String => $Body, ); # display inline images if exists my $SessionID = ''; if ( $Self->{SessionID} && !$Self->{SessionIDCookie} ) { $SessionID = ';' . $Self->{SessionName} . '=' . $Self->{SessionID}; } my $AttachmentLink = $Self->{Baselink} . 'Action=PictureUpload' . ';FormID=' . $Param{FormID} . $SessionID . ';ContentID='; # search inline documents in body and add it to upload cache my %Attachments = %{ $QuoteArticle{Atms} }; my %AttachmentAlreadyUsed; $Body =~ s{ (=|"|')cid:(.*?)("|'|>|\/>|\s) } { my $Start= $1; my $ContentID = $2; my $End = $3; # improve html quality if ( $Start ne '"' && $Start ne '\'' ) { $Start .= '"'; } if ( $End ne '"' && $End ne '\'' ) { $End = '"' . $End; } # find attachment to include ATMCOUNT: for my $AttachmentID ( sort keys %Attachments ) { if ( lc $Attachments{$AttachmentID}->{ContentID} ne lc "<$ContentID>" ) { next ATMCOUNT; } # get whole attachment my %AttachmentPicture = $ArticleBackendObject->ArticleAttachment( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, FileID => $AttachmentID, ); # content id cleanup $AttachmentPicture{ContentID} =~ s/^$//; # find cid, add attachment URL and remember, file is already uploaded $ContentID = $AttachmentLink . $Self->LinkEncode( $AttachmentPicture{ContentID} ); # add to upload cache if not uploaded and remember if (!$AttachmentAlreadyUsed{$AttachmentID}) { # remember $AttachmentAlreadyUsed{$AttachmentID} = 1; # write attachment to upload cache $Param{UploadCacheObject}->FormIDAddFile( FormID => $Param{FormID}, Disposition => 'inline', %{ $Attachments{$AttachmentID} }, %AttachmentPicture, ); } } # return link $Start . $ContentID . $End; }egxi; # find inline images using Content-Location instead of Content-ID ATTACHMENT: for my $AttachmentID ( sort keys %Attachments ) { next ATTACHMENT if !$Attachments{$AttachmentID}->{ContentID}; # get whole attachment my %AttachmentPicture = $ArticleBackendObject->ArticleAttachment( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, FileID => $AttachmentID, ); # content id cleanup $AttachmentPicture{ContentID} =~ s/^$//; $Body =~ s{ ("|')(\Q$AttachmentPicture{ContentID}\E)("|'|>|\/>|\s) } { my $Start= $1; my $ContentID = $2; my $End = $3; # find cid, add attachment URL and remember, file is already uploaded $ContentID = $AttachmentLink . $Self->LinkEncode( $AttachmentPicture{ContentID} ); # add to upload cache if not uploaded and remember if (!$AttachmentAlreadyUsed{$AttachmentID}) { # remember $AttachmentAlreadyUsed{$AttachmentID} = 1; # write attachment to upload cache $Param{UploadCacheObject}->FormIDAddFile( FormID => $Param{FormID}, Disposition => 'inline', %{ $Attachments{$AttachmentID} }, %AttachmentPicture, ); } # return link $Start . $ContentID . $End; }egxi; } # find not inline images ATTACHMENT: for my $AttachmentID ( sort keys %Attachments ) { next ATTACHMENT if $AttachmentAlreadyUsed{$AttachmentID}; $NotInlineAttachments{$AttachmentID} = 1; } } # attach also other attachments on article forward if ( $Body && $Param{AttachmentsInclude} ) { for my $AttachmentID ( sort keys %NotInlineAttachments ) { my %Attachment = $ArticleBackendObject->ArticleAttachment( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, FileID => $AttachmentID, ); # add attachment $Param{UploadCacheObject}->FormIDAddFile( FormID => $Param{FormID}, %Attachment, Disposition => 'attachment', ); } } # Fallback for non-MIMEBase articles: get article HTML content if it exists. if ( !$Body ) { $Body = $Self->ArticlePreview( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, ); } return $Body if $Body; } # as fallback use text body for quote my %Article = $ArticleBackendObject->ArticleGet( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, DynamicFields => 0, ); # check if original content isn't text/plain or text/html, don't use it if ( !$Article{ContentType} ) { $Article{ContentType} = 'text/plain'; } if ( $Article{ContentType} !~ /text\/(plain|html)/i ) { $Article{Body} = '-> no quotable message <-'; $Article{ContentType} = 'text/plain'; } else { $Article{Body} = $Self->WrapPlainText( MaxCharacters => $ConfigObject->Get('Ticket::Frontend::TextAreaEmail') || 82, PlainText => $Article{Body}, ); } # attach attachments if ( $Param{AttachmentsInclude} ) { my %ArticleIndex = $ArticleBackendObject->ArticleAttachmentIndex( ArticleID => $Param{ArticleID}, ExcludePlainText => 1, ExcludeHTMLBody => 1, ); for my $Index ( sort keys %ArticleIndex ) { my %Attachment = $ArticleBackendObject->ArticleAttachment( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, FileID => $Index, ); # add attachment $Param{UploadCacheObject}->FormIDAddFile( FormID => $Param{FormID}, %Attachment, Disposition => 'attachment', ); } } # return body as html if ( $ConfigObject->Get('Frontend::RichText') ) { $Article{Body} = $Self->Ascii2Html( Text => $Article{Body}, HTMLResultMode => 1, LinkFeature => 1, ); } # return body as plain text return $Article{Body}; } sub _BackendGet { my ( $Self, %Param ) = @_; # Check needed stuff. for my $Needed (qw(TicketID ArticleID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForArticle(%Param); # Determine channel name for this article. my $ChannelName = $ArticleBackendObject->ChannelNameGet(); my $Loaded = $Kernel::OM->Get('Kernel::System::Main')->Require( "Kernel::Output::HTML::Article::$ChannelName", ); return if !$Loaded; return $Kernel::OM->Get("Kernel::Output::HTML::Article::$ChannelName"); } 1; =head1 TERMS AND CONDITIONS This software is part of the OTRS project (L). This software comes with ABSOLUTELY NO WARRANTY. For details, see the enclosed file COPYING for license information (GPL). If you did not receive this file, see L. =cut