Files
scripts/Perl OTRS/Kernel/Output/HTML/Layout/Article.pm
2024-10-14 00:08:40 +02:00

573 lines
17 KiB
Perl

# --
# Copyright (C) 2001-2019 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<https://otrs.org/>).
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<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut