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