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::System::Ticket::Article::Backend::MIMEBase::Base;
10
11use strict;
12use warnings;
13
14our $ObjectManagerDisabled = 1;
15
16=head1 NAME
17
18Kernel::System::Ticket::Article::Backend::MIMEBase::Base - base class for article storage modules
19
20=head1 DESCRIPTION
21
22This is a base class for article storage backends and should not be instantiated directly.
23
24=head1 PUBLIC INTERFACE
25
26=cut
27
28=head2 new()
29
30Don't instantiate this class directly, get instances of the real storage backends instead:
31
32    my $BackendObject = $Kernel::OM->Get('Kernel::System::Article::Backend::MIMEBase::ArticleStorageDB');
33
34=cut
35
36sub new {
37    my ( $Type, %Param ) = @_;
38
39    # allocate new hash for object
40    my $Self = {};
41    bless( $Self, $Type );
42
43    $Self->{CacheType} = 'ArticleStorageBase';
44    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;
45
46    $Self->{ArticleDataDir}
47        = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir')
48        || die 'Got no ArticleDataDir!';
49
50    # do we need to check all backends, or just one?
51    $Self->{CheckAllBackends}
52        = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::CheckAllStorageBackends')
53        // 0;
54
55    return $Self;
56}
57
58=head2 BuildArticleContentPath()
59
60Generate a base article content path for article storage in the file system.
61
62    my $ArticleContentPath = $BackendObject->BuildArticleContentPath();
63
64=cut
65
66sub BuildArticleContentPath {
67    my ( $Self, %Param ) = @_;
68
69    return $Self->{ArticleContentPath} if $Self->{ArticleContentPath};
70
71    $Self->{ArticleContentPath} = $Kernel::OM->Create('Kernel::System::DateTime')->Format(
72        Format => '%Y/%m/%d',
73    );
74
75    return $Self->{ArticleContentPath};
76}
77
78=head2 ArticleAttachmentIndex()
79
80Get article attachment index as hash.
81
82    my %Index = $BackendObject->ArticleAttachmentIndex(
83        ArticleID        => 123,
84        ExcludePlainText => 1,       # (optional) Exclude plain text attachment
85        ExcludeHTMLBody  => 1,       # (optional) Exclude HTML body attachment
86        ExcludeInline    => 1,       # (optional) Exclude inline attachments
87        OnlyHTMLBody     => 1,       # (optional) Return only HTML body attachment, return nothing if not found
88    );
89
90Returns:
91
92    my %Index = {
93        '1' => {                                                # Attachment ID
94            ContentAlternative => '',                           # (optional)
95            ContentID          => '',                           # (optional)
96            ContentType        => 'application/pdf',
97            Filename           => 'StdAttachment-Test1.pdf',
98            FilesizeRaw        => 4722,
99            Disposition        => 'attachment',
100        },
101        '2' => {
102            ContentAlternative => '',
103            ContentID          => '',
104            ContentType        => 'text/html; charset="utf-8"',
105            Filename           => 'file-2',
106            FilesizeRaw        => 183,
107            Disposition        => 'attachment',
108        },
109        ...
110    };
111
112=cut
113
114sub ArticleAttachmentIndex {
115    my ( $Self, %Param ) = @_;
116
117    if ( !$Param{ArticleID} ) {
118        $Kernel::OM->Get('Kernel::System::Log')->Log(
119            Priority => 'error',
120            Message  => 'Need ArticleID!',
121        );
122        return;
123    }
124
125    if ( $Param{ExcludeHTMLBody} && $Param{OnlyHTMLBody} ) {
126        $Kernel::OM->Get('Kernel::System::Log')->Log(
127            Priority => 'error',
128            Message  => 'ExcludeHTMLBody and OnlyHTMLBody cannot be used together!',
129        );
130        return;
131    }
132
133    # Get complete attachment index from backend.
134    my %Attachments = $Self->ArticleAttachmentIndexRaw(%Param);
135
136    # Iterate over attachments only if any of optional parameters is active.
137    if ( $Param{ExcludePlainText} || $Param{ExcludeHTMLBody} || $Param{ExcludeInline} || $Param{OnlyHTMLBody} ) {
138
139        my $AttachmentIDPlain = 0;
140        my $AttachmentIDHTML  = 0;
141
142        ATTACHMENT_ID:
143        for my $AttachmentID ( sort keys %Attachments ) {
144            my %File = %{ $Attachments{$AttachmentID} };
145
146            # Identify plain text attachment.
147            if (
148                !$AttachmentIDPlain
149                &&
150                $File{Filename} eq 'file-1'
151                && $File{ContentType} =~ /text\/plain/i
152                && $File{Disposition} eq 'inline'
153                )
154            {
155                $AttachmentIDPlain = $AttachmentID;
156                next ATTACHMENT_ID;
157            }
158
159            # Identify html body attachment:
160            #   - file-[12] is plain+html attachment
161            #   - file-1.html is html attachment only
162            if (
163                !$AttachmentIDHTML
164                &&
165                ( $File{Filename} =~ /^file-[12]$/ || $File{Filename} eq 'file-1.html' )
166                && $File{ContentType} =~ /text\/html/i
167                && $File{Disposition} eq 'inline'
168                )
169            {
170                $AttachmentIDHTML = $AttachmentID;
171                next ATTACHMENT_ID;
172            }
173        }
174
175        # If neither plain text or html body were found, iterate again to try to identify plain text among regular
176        #   non-inline attachments.
177        if ( !$AttachmentIDPlain && !$AttachmentIDHTML ) {
178            ATTACHMENT_ID:
179            for my $AttachmentID ( sort keys %Attachments ) {
180                my %File = %{ $Attachments{$AttachmentID} };
181
182                # Remember, file-1 got defined by parsing if no filename was given.
183                if (
184                    $File{Filename} eq 'file-1'
185                    && $File{ContentType} =~ /text\/plain/i
186                    )
187                {
188                    $AttachmentIDPlain = $AttachmentID;
189                    last ATTACHMENT_ID;
190                }
191            }
192        }
193
194        # Identify inline (image) attachments which are referenced in HTML body. Do not strip attachments based on their
195        #   disposition, since this method of detection is unreliable. Please see bug#13353 for more information.
196        my @AttachmentIDsInline;
197
198        if ($AttachmentIDHTML) {
199
200            # Get HTML article body.
201            my %HTMLBody = $Self->ArticleAttachment(
202                ArticleID => $Param{ArticleID},
203                FileID    => $AttachmentIDHTML,
204            );
205
206            if ( %HTMLBody && $HTMLBody{Content} ) {
207
208                ATTACHMENT_ID:
209                for my $AttachmentID ( sort keys %Attachments ) {
210                    my %File = %{ $Attachments{$AttachmentID} };
211
212                    next ATTACHMENT_ID if $File{ContentType} !~ m{image}ixms;
213                    next ATTACHMENT_ID if !$File{ContentID};
214
215                    my ($ImageID) = ( $File{ContentID} =~ m{^<(.*)>$}ixms );
216
217                    # Search in the article body if there is any reference to it.
218                    if ( $HTMLBody{Content} =~ m{<img.+src=['|"]cid:\Q$ImageID\E['|"].*>}ixms ) {
219                        push @AttachmentIDsInline, $AttachmentID;
220                    }
221                }
222            }
223        }
224
225        if ( $AttachmentIDPlain && $Param{ExcludePlainText} ) {
226            delete $Attachments{$AttachmentIDPlain};
227        }
228
229        if ( $AttachmentIDHTML && $Param{ExcludeHTMLBody} ) {
230            delete $Attachments{$AttachmentIDHTML};
231        }
232
233        if ( $Param{ExcludeInline} ) {
234            for my $AttachmentID (@AttachmentIDsInline) {
235                delete $Attachments{$AttachmentID};
236            }
237        }
238
239        if ( $Param{OnlyHTMLBody} ) {
240            if ($AttachmentIDHTML) {
241                %Attachments = (
242                    $AttachmentIDHTML => $Attachments{$AttachmentIDHTML}
243                );
244            }
245            else {
246                %Attachments = ();
247            }
248        }
249    }
250
251    return %Attachments;
252}
253
254=head1 PRIVATE FUNCTIONS
255
256=cut
257
258sub _ArticleDeleteDirectory {
259    my ( $Self, %Param ) = @_;
260
261    for my $Needed (qw(ArticleID UserID)) {
262        if ( !$Param{$Needed} ) {
263            $Kernel::OM->Get('Kernel::System::Log')->Log(
264                Priority => 'error',
265                Message  => "Need $Needed!"
266            );
267            return;
268        }
269    }
270
271    # delete directory from fs
272    my $ContentPath = $Self->_ArticleContentPathGet(
273        ArticleID => $Param{ArticleID},
274    );
275    my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
276    if ( -d $Path ) {
277        if ( !rmdir $Path ) {
278            $Kernel::OM->Get('Kernel::System::Log')->Log(
279                Priority => 'error',
280                Message  => "Can't remove '$Path': $!.",
281            );
282            return;
283        }
284    }
285    return 1;
286}
287
288=head2 _ArticleContentPathGet()
289
290Get the stored content path of an article.
291
292    my $Path = $BackendObject->_ArticleContentPatGeth(
293        ArticleID => 123,
294    );
295
296=cut
297
298sub _ArticleContentPathGet {
299    my ( $Self, %Param ) = @_;
300
301    if ( !$Param{ArticleID} ) {
302        $Kernel::OM->Get('Kernel::System::Log')->Log(
303            Priority => 'error',
304            Message  => 'Need ArticleID!',
305        );
306        return;
307    }
308
309    # check key
310    my $CacheKey = '_ArticleContentPathGet::' . $Param{ArticleID};
311
312    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
313
314    # check cache
315    my $Cache = $CacheObject->Get(
316        Type => $Self->{CacheType},
317        Key  => $CacheKey,
318    );
319    return $Cache if $Cache;
320
321    # get database object
322    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
323
324    # sql query
325    return if !$DBObject->Prepare(
326        SQL  => 'SELECT content_path FROM article_data_mime WHERE article_id = ?',
327        Bind => [ \$Param{ArticleID} ],
328    );
329
330    my $Result;
331    while ( my @Row = $DBObject->FetchrowArray() ) {
332        $Result = $Row[0];
333    }
334
335    # set cache
336    $CacheObject->Set(
337        Type  => $Self->{CacheType},
338        TTL   => $Self->{CacheTTL},
339        Key   => $CacheKey,
340        Value => $Result,
341    );
342
343    # return
344    return $Result;
345}
346
3471;
348
349=head1 TERMS AND CONDITIONS
350
351This software is part of the OTRS project (L<https://otrs.org/>).
352
353This software comes with ABSOLUTELY NO WARRANTY. For details, see
354the enclosed file COPYING for license information (GPL). If you
355did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
356
357=cut
358