# --
# 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/^/;
$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/^/;
$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