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::ArticleStorageFS;
10
11use strict;
12use warnings;
13
14use File::Path qw();
15use MIME::Base64 qw();
16use Time::HiRes qw();
17use Unicode::Normalize qw();
18
19use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base);
20
21use Kernel::System::VariableCheck qw(:all);
22
23our @ObjectDependencies = (
24    'Kernel::Config',
25    'Kernel::System::Cache',
26    'Kernel::System::DB',
27    'Kernel::System::DynamicFieldValue',
28    'Kernel::System::Encode',
29    'Kernel::System::Log',
30    'Kernel::System::Main',
31    'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB',
32);
33
34=head1 NAME
35
36Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS - FS based ticket article storage interface
37
38=head1 DESCRIPTION
39
40This class provides functions to manipulate ticket articles on the file system.
41The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>.
42
43Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>.
44
45See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB>.
46
47=cut
48
49sub new {
50    my ( $Type, %Param ) = @_;
51
52    # Call new() on Base.pm to execute the common code.
53    my $Self = $Type->SUPER::new(%Param);
54
55    my $ArticleContentPath = $Self->BuildArticleContentPath();
56    my $ArticleDir         = "$Self->{ArticleDataDir}/$ArticleContentPath/";
57
58    File::Path::mkpath( $ArticleDir, 0, 0770 );    ## no critic
59
60    # Check write permissions.
61    if ( !-w $ArticleDir ) {
62
63        $Kernel::OM->Get('Kernel::System::Log')->Log(
64            Priority => 'notice',
65            Message  => "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!",
66        );
67        die "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!";
68    }
69
70    # Get activated cache backend configuration.
71    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
72    return $Self if !$ConfigObject->Get('Cache::ArticleStorageCache');
73
74    my $CacheModule = $ConfigObject->Get('Cache::Module') || '';
75    return $Self if $CacheModule ne 'Kernel::System::Cache::MemcachedFast';
76
77    # Turn on special cache used for speeding up article storage methods in huge systems with many
78    #   nodes and slow FS access. It will be used only in environments with configured Memcached
79    #   backend (see config above).
80    $Self->{ArticleStorageCache}    = 1;
81    $Self->{ArticleStorageCacheTTL} = $ConfigObject->Get('Cache::ArticleStorageCache::TTL') || 60 * 60 * 24;
82
83    return $Self;
84}
85
86sub ArticleDelete {
87    my ( $Self, %Param ) = @_;
88
89    # check needed stuff
90    for my $Item (qw(ArticleID UserID)) {
91        if ( !$Param{$Item} ) {
92            $Kernel::OM->Get('Kernel::System::Log')->Log(
93                Priority => 'error',
94                Message  => "Need $Item!",
95            );
96            return;
97        }
98    }
99
100    # delete attachments
101    $Self->ArticleDeleteAttachment(
102        ArticleID => $Param{ArticleID},
103        UserID    => $Param{UserID},
104    );
105
106    # delete plain message
107    $Self->ArticleDeletePlain(
108        ArticleID => $Param{ArticleID},
109        UserID    => $Param{UserID},
110    );
111
112    # delete storage directory
113    $Self->_ArticleDeleteDirectory(
114        ArticleID => $Param{ArticleID},
115        UserID    => $Param{UserID},
116    );
117
118    # Delete special article storage cache.
119    if ( $Self->{ArticleStorageCache} ) {
120        $Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
121            Type => 'ArticleStorageFS_' . $Param{ArticleID},
122        );
123    }
124
125    return 1;
126}
127
128sub ArticleDeletePlain {
129    my ( $Self, %Param ) = @_;
130
131    # check needed stuff
132    for my $Item (qw(ArticleID UserID)) {
133        if ( !$Param{$Item} ) {
134            $Kernel::OM->Get('Kernel::System::Log')->Log(
135                Priority => 'error',
136                Message  => "Need $Item!",
137            );
138            return;
139        }
140    }
141
142    # delete from fs
143    my $ContentPath = $Self->_ArticleContentPathGet(
144        ArticleID => $Param{ArticleID},
145    );
146    my $File = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt";
147    if ( -f $File ) {
148        if ( !unlink $File ) {
149            $Kernel::OM->Get('Kernel::System::Log')->Log(
150                Priority => 'error',
151                Message  => "Can't remove: $File: $!!",
152            );
153            return;
154        }
155    }
156
157    # Delete special article storage cache.
158    if ( $Self->{ArticleStorageCache} ) {
159        $Kernel::OM->Get('Kernel::System::Cache')->Delete(
160            Type => 'ArticleStorageFS_' . $Param{ArticleID},
161            Key  => 'ArticlePlain',
162        );
163    }
164
165    # return if only delete in my backend
166    return 1 if $Param{OnlyMyBackend};
167
168    return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleDeletePlain(
169        %Param,
170        OnlyMyBackend => 1,
171    );
172}
173
174sub ArticleDeleteAttachment {
175    my ( $Self, %Param ) = @_;
176
177    # check needed stuff
178    for my $Item (qw(ArticleID UserID)) {
179        if ( !$Param{$Item} ) {
180            $Kernel::OM->Get('Kernel::System::Log')->Log(
181                Priority => 'error',
182                Message  => "Need $Item!",
183            );
184            return;
185        }
186    }
187
188    # delete from fs
189    my $ContentPath = $Self->_ArticleContentPathGet(
190        ArticleID => $Param{ArticleID},
191    );
192    my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
193
194    if ( -e $Path ) {
195
196        my @List = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
197            Directory => $Path,
198            Filter    => "*",
199        );
200
201        for my $File (@List) {
202
203            if ( $File !~ /(\/|\\)plain.txt$/ ) {
204
205                if ( !unlink "$File" ) {
206
207                    $Kernel::OM->Get('Kernel::System::Log')->Log(
208                        Priority => 'error',
209                        Message  => "Can't remove: $File: $!!",
210                    );
211                }
212            }
213        }
214    }
215
216    # Delete special article storage cache.
217    if ( $Self->{ArticleStorageCache} ) {
218        $Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
219            Type => 'ArticleStorageFS_' . $Param{ArticleID},
220        );
221    }
222
223    # return if only delete in my backend
224    return 1 if $Param{OnlyMyBackend};
225
226    return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
227        ->ArticleDeleteAttachment(
228        %Param,
229        OnlyMyBackend => 1,
230        );
231}
232
233sub ArticleWritePlain {
234    my ( $Self, %Param ) = @_;
235
236    # check needed stuff
237    for my $Item (qw(ArticleID Email UserID)) {
238        if ( !$Param{$Item} ) {
239            $Kernel::OM->Get('Kernel::System::Log')->Log(
240                Priority => 'error',
241                Message  => "Need $Item!",
242            );
243            return;
244        }
245    }
246
247    # prepare/filter ArticleID
248    $Param{ArticleID} = quotemeta( $Param{ArticleID} );
249    $Param{ArticleID} =~ s/\0//g;
250
251    # define path
252    my $ContentPath = $Self->_ArticleContentPathGet(
253        ArticleID => $Param{ArticleID},
254    );
255    my $Path = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
256
257    # debug
258    if ( defined $Self->{Debug} && $Self->{Debug} > 1 ) {
259        $Kernel::OM->Get('Kernel::System::Log')->Log( Message => "->WriteArticle: $Path" );
260    }
261
262    # write article to fs 1:1
263    File::Path::mkpath( [$Path], 0, 0770 );    ## no critic
264
265    # write article to fs
266    my $Success = $Kernel::OM->Get('Kernel::System::Main')->FileWrite(
267        Location   => "$Path/plain.txt",
268        Mode       => 'binmode',
269        Content    => \$Param{Email},
270        Permission => '660',
271    );
272
273    # Write to special article storage cache.
274    if ( $Self->{ArticleStorageCache} ) {
275        $Kernel::OM->Get('Kernel::System::Cache')->Set(
276            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
277            TTL            => $Self->{ArticleStorageCacheTTL},
278            Key            => 'ArticlePlain',
279            Value          => $Param{Email},
280            CacheInMemory  => 0,
281            CacheInBackend => 1,
282        );
283    }
284
285    return if !$Success;
286
287    return 1;
288}
289
290sub ArticleWriteAttachment {
291    my ( $Self, %Param ) = @_;
292
293    # check needed stuff
294    for my $Item (qw(Filename ContentType ArticleID UserID)) {
295        if ( !IsStringWithData( $Param{$Item} ) ) {
296            $Kernel::OM->Get('Kernel::System::Log')->Log(
297                Priority => 'error',
298                Message  => "Need $Item!",
299            );
300            return;
301        }
302    }
303
304    # prepare/filter ArticleID
305    $Param{ArticleID} = quotemeta( $Param{ArticleID} );
306    $Param{ArticleID} =~ s/\0//g;
307    my $ContentPath = $Self->_ArticleContentPathGet(
308        ArticleID => $Param{ArticleID},
309    );
310
311    # define path
312    $Param{Path} = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
313
314    # get main object
315    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
316
317    # Perform FilenameCleanup here already to check for
318    #   conflicting existing attachment files correctly
319    $Param{Filename} = $MainObject->FilenameCleanUp(
320        Filename => $Param{Filename},
321        Type     => 'Local',
322    );
323
324    my $NewFileName = $Param{Filename};
325    my %UsedFile;
326    my %Index = $Self->ArticleAttachmentIndex(
327        ArticleID => $Param{ArticleID},
328    );
329
330    # Normalize filenames to find file names which are identical but in a different unicode form.
331    #   This is needed because Mac OS (HFS+) converts all filenames to NFD internally.
332    #   Without this, the same file might be overwritten because the strings are not equal.
333    for my $Position ( sort keys %Index ) {
334        $UsedFile{ Unicode::Normalize::NFC( $Index{$Position}->{Filename} ) } = 1;
335    }
336    for ( my $i = 1; $i <= 50; $i++ ) {
337        if ( exists $UsedFile{ Unicode::Normalize::NFC($NewFileName) } ) {
338            if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) {
339                $NewFileName = "$1-$i.$2";
340            }
341            else {
342                $NewFileName = "$Param{Filename}-$i";
343            }
344        }
345    }
346
347    $Param{Filename} = $NewFileName;
348
349    # write attachment to backend
350    if ( !-d $Param{Path} ) {
351        if ( !File::Path::mkpath( [ $Param{Path} ], 0, 0770 ) ) {    ## no critic
352            $Kernel::OM->Get('Kernel::System::Log')->Log(
353                Priority => 'error',
354                Message  => "Can't create $Param{Path}: $!",
355            );
356            return;
357        }
358    }
359
360    # write attachment content type to fs
361    my $SuccessContentType = $MainObject->FileWrite(
362        Directory       => $Param{Path},
363        Filename        => "$Param{Filename}.content_type",
364        Mode            => 'binmode',
365        Content         => \$Param{ContentType},
366        Permission      => 660,
367        NoFilenameClean => 1,
368    );
369    return if !$SuccessContentType;
370
371    # set content id in angle brackets
372    if ( $Param{ContentID} ) {
373        $Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/;
374    }
375
376    # write attachment content id to fs
377    if ( $Param{ContentID} ) {
378        $MainObject->FileWrite(
379            Directory       => $Param{Path},
380            Filename        => "$Param{Filename}.content_id",
381            Mode            => 'binmode',
382            Content         => \$Param{ContentID},
383            Permission      => 660,
384            NoFilenameClean => 1,
385        );
386    }
387
388    # write attachment content alternative to fs
389    if ( $Param{ContentAlternative} ) {
390        $MainObject->FileWrite(
391            Directory       => $Param{Path},
392            Filename        => "$Param{Filename}.content_alternative",
393            Mode            => 'binmode',
394            Content         => \$Param{ContentAlternative},
395            Permission      => 660,
396            NoFilenameClean => 1,
397        );
398    }
399
400    # write attachment disposition to fs
401    if ( $Param{Disposition} ) {
402
403        my ( $Disposition, $FileName ) = split ';', $Param{Disposition};
404
405        $MainObject->FileWrite(
406            Directory       => $Param{Path},
407            Filename        => "$Param{Filename}.disposition",
408            Mode            => 'binmode',
409            Content         => \$Disposition || '',
410            Permission      => 660,
411            NoFilenameClean => 1,
412        );
413    }
414
415    # write attachment content to fs
416    my $SuccessContent = $MainObject->FileWrite(
417        Directory  => $Param{Path},
418        Filename   => $Param{Filename},
419        Mode       => 'binmode',
420        Content    => \$Param{Content},
421        Permission => 660,
422    );
423
424    return if !$SuccessContent;
425
426    # Delete special article storage cache.
427    if ( $Self->{ArticleStorageCache} ) {
428        $Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
429            Type => 'ArticleStorageFS_' . $Param{ArticleID},
430        );
431    }
432
433    return 1;
434}
435
436sub ArticlePlain {
437    my ( $Self, %Param ) = @_;
438
439    # check needed stuff
440    if ( !$Param{ArticleID} ) {
441        $Kernel::OM->Get('Kernel::System::Log')->Log(
442            Priority => 'error',
443            Message  => 'Need ArticleID!',
444        );
445        return;
446    }
447
448    # prepare/filter ArticleID
449    $Param{ArticleID} = quotemeta( $Param{ArticleID} );
450    $Param{ArticleID} =~ s/\0//g;
451
452    # get cache object
453    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
454
455    # Read from special article storage cache.
456    if ( $Self->{ArticleStorageCache} ) {
457        my $Cache = $CacheObject->Get(
458            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
459            Key            => 'ArticlePlain',
460            CacheInMemory  => 0,
461            CacheInBackend => 1,
462        );
463
464        return $Cache if $Cache;
465    }
466
467    # get content path
468    my $ContentPath = $Self->_ArticleContentPathGet(
469        ArticleID => $Param{ArticleID},
470    );
471
472    # open plain article
473    if ( -f "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt" ) {
474
475        # read whole article
476        my $Data = $Kernel::OM->Get('Kernel::System::Main')->FileRead(
477            Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/",
478            Filename  => 'plain.txt',
479            Mode      => 'binmode',
480        );
481
482        return if !$Data;
483
484        # Write to special article storage cache.
485        if ( $Self->{ArticleStorageCache} ) {
486            $CacheObject->Set(
487                Type           => 'ArticleStorageFS_' . $Param{ArticleID},
488                TTL            => $Self->{ArticleStorageCacheTTL},
489                Key            => 'ArticlePlain',
490                Value          => ${$Data},
491                CacheInMemory  => 0,
492                CacheInBackend => 1,
493            );
494        }
495
496        return ${$Data};
497    }
498
499    # return if we only need to check one backend
500    return if !$Self->{CheckAllBackends};
501
502    # return if only delete in my backend
503    return if $Param{OnlyMyBackend};
504
505    my $Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticlePlain(
506        %Param,
507        OnlyMyBackend => 1,
508    );
509
510    # Write to special article storage cache.
511    if ( $Self->{ArticleStorageCache} ) {
512        $CacheObject->Set(
513            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
514            TTL            => $Self->{ArticleStorageCacheTTL},
515            Key            => 'ArticlePlain',
516            Value          => $Data,
517            CacheInMemory  => 0,
518            CacheInBackend => 1,
519        );
520    }
521
522    return $Data;
523}
524
525sub ArticleAttachmentIndexRaw {
526    my ( $Self, %Param ) = @_;
527
528    # check needed stuff
529    if ( !$Param{ArticleID} ) {
530        $Kernel::OM->Get('Kernel::System::Log')->Log(
531            Priority => 'error',
532            Message  => 'Need ArticleID!',
533        );
534        return;
535    }
536
537    # get cache object
538    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
539
540    # Read from special article storage cache.
541    if ( $Self->{ArticleStorageCache} ) {
542        my $Cache = $CacheObject->Get(
543            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
544            Key            => 'ArticleAttachmentIndexRaw',
545            CacheInMemory  => 0,
546            CacheInBackend => 1,
547        );
548
549        return %{$Cache} if $Cache;
550    }
551
552    my $ContentPath = $Self->_ArticleContentPathGet(
553        ArticleID => $Param{ArticleID},
554    );
555    my %Index;
556    my $Counter = 0;
557
558    # get main object
559    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
560
561    # try fs
562    my @List = $MainObject->DirectoryRead(
563        Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
564        Filter    => "*",
565        Silent    => 1,
566    );
567
568    FILENAME:
569    for my $Filename ( sort @List ) {
570        my $FileSizeRaw = -s $Filename;
571
572        # do not use control file
573        next FILENAME if $Filename =~ /\.content_alternative$/;
574        next FILENAME if $Filename =~ /\.content_id$/;
575        next FILENAME if $Filename =~ /\.content_type$/;
576        next FILENAME if $Filename =~ /\.disposition$/;
577        next FILENAME if $Filename =~ /\/plain.txt$/;
578
579        # read content type
580        my $ContentType = '';
581        my $ContentID   = '';
582        my $Alternative = '';
583        my $Disposition = '';
584        if ( -e "$Filename.content_type" ) {
585            my $Content = $MainObject->FileRead(
586                Location => "$Filename.content_type",
587            );
588            return if !$Content;
589            $ContentType = ${$Content};
590
591            # content id (optional)
592            if ( -e "$Filename.content_id" ) {
593                my $Content = $MainObject->FileRead(
594                    Location => "$Filename.content_id",
595                );
596                if ($Content) {
597                    $ContentID = ${$Content};
598                }
599            }
600
601            # alternative (optional)
602            if ( -e "$Filename.content_alternative" ) {
603                my $Content = $MainObject->FileRead(
604                    Location => "$Filename.content_alternative",
605                );
606                if ($Content) {
607                    $Alternative = ${$Content};
608                }
609            }
610
611            # disposition
612            if ( -e "$Filename.disposition" ) {
613                my $Content = $MainObject->FileRead(
614                    Location => "$Filename.disposition",
615                );
616                if ($Content) {
617                    $Disposition = ${$Content};
618                }
619            }
620
621            # if no content disposition is set images with content id should be inline
622            elsif ( $ContentID && $ContentType =~ m{image}i ) {
623                $Disposition = 'inline';
624            }
625
626            # converted article body should be inline
627            elsif ( $Filename =~ m{file-[12]} ) {
628                $Disposition = 'inline';
629            }
630
631            # all others including attachments with content id that are not images
632            #   should NOT be inline
633            else {
634                $Disposition = 'attachment';
635            }
636        }
637
638        # read content type (old style)
639        else {
640            my $Content = $MainObject->FileRead(
641                Location => $Filename,
642                Result   => 'ARRAY',
643            );
644            if ( !$Content ) {
645                return;
646            }
647            $ContentType = $Content->[0];
648        }
649
650        # strip filename
651        $Filename =~ s!^.*/!!;
652
653        # add the info the the hash
654        $Counter++;
655        $Index{$Counter} = {
656            Filename           => $Filename,
657            FilesizeRaw        => $FileSizeRaw,
658            ContentType        => $ContentType,
659            ContentID          => $ContentID,
660            ContentAlternative => $Alternative,
661            Disposition        => $Disposition,
662        };
663    }
664
665    # Write to special article storage cache.
666    if ( $Self->{ArticleStorageCache} ) {
667        $CacheObject->Set(
668            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
669            TTL            => $Self->{ArticleStorageCacheTTL},
670            Key            => 'ArticleAttachmentIndexRaw',
671            Value          => \%Index,
672            CacheInMemory  => 0,
673            CacheInBackend => 1,
674        );
675    }
676
677    return %Index if %Index;
678
679    # return if we only need to check one backend
680    return if !$Self->{CheckAllBackends};
681
682    # return if only delete in my backend
683    return %Index if $Param{OnlyMyBackend};
684
685    %Index = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
686        ->ArticleAttachmentIndexRaw(
687        %Param,
688        OnlyMyBackend => 1,
689        );
690
691    # Write to special article storage cache.
692    if ( $Self->{ArticleStorageCache} ) {
693        $CacheObject->Set(
694            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
695            TTL            => $Self->{ArticleStorageCacheTTL},
696            Key            => 'ArticleAttachmentIndexRaw',
697            Value          => \%Index,
698            CacheInMemory  => 0,
699            CacheInBackend => 1,
700        );
701    }
702
703    return %Index;
704}
705
706sub ArticleAttachment {
707    my ( $Self, %Param ) = @_;
708
709    # check needed stuff
710    for my $Item (qw(ArticleID FileID)) {
711        if ( !$Param{$Item} ) {
712            $Kernel::OM->Get('Kernel::System::Log')->Log(
713                Priority => 'error',
714                Message  => "Need $Item!",
715            );
716            return;
717        }
718    }
719
720    # prepare/filter ArticleID
721    $Param{ArticleID} = quotemeta( $Param{ArticleID} );
722    $Param{ArticleID} =~ s/\0//g;
723
724    # get cache object
725    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
726
727    # Read from special article storage cache.
728    if ( $Self->{ArticleStorageCache} ) {
729        my $Cache = $CacheObject->Get(
730            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
731            Key            => 'ArticleAttachment' . $Param{FileID},
732            CacheInMemory  => 0,
733            CacheInBackend => 1,
734        );
735
736        return %{$Cache} if $Cache;
737    }
738
739    # get attachment index
740    my %Index = $Self->ArticleAttachmentIndex(
741        ArticleID => $Param{ArticleID},
742    );
743
744    # get content path
745    my $ContentPath = $Self->_ArticleContentPathGet(
746        ArticleID => $Param{ArticleID},
747    );
748    my %Data    = %{ $Index{ $Param{FileID} } // {} };
749    my $Counter = 0;
750
751    # get main object
752    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
753
754    my @List = $MainObject->DirectoryRead(
755        Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
756        Filter    => "*",
757        Silent    => 1,
758    );
759
760    if (@List) {
761
762        # get encode object
763        my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');
764
765        FILENAME:
766        for my $Filename (@List) {
767            next FILENAME if $Filename =~ /\.content_alternative$/;
768            next FILENAME if $Filename =~ /\.content_id$/;
769            next FILENAME if $Filename =~ /\.content_type$/;
770            next FILENAME if $Filename =~ /\/plain.txt$/;
771            next FILENAME if $Filename =~ /\.disposition$/;
772
773            # add the info the the hash
774            $Counter++;
775            if ( $Counter == $Param{FileID} ) {
776
777                if ( -e "$Filename.content_type" ) {
778
779                    # read content type
780                    my $Content = $MainObject->FileRead(
781                        Location => "$Filename.content_type",
782                    );
783                    return if !$Content;
784                    $Data{ContentType} = ${$Content};
785
786                    # read content
787                    $Content = $MainObject->FileRead(
788                        Location => $Filename,
789                        Mode     => 'binmode',
790                    );
791                    return if !$Content;
792                    $Data{Content} = ${$Content};
793
794                    # content id (optional)
795                    if ( -e "$Filename.content_id" ) {
796                        my $Content = $MainObject->FileRead(
797                            Location => "$Filename.content_id",
798                        );
799                        if ($Content) {
800                            $Data{ContentID} = ${$Content};
801                        }
802                    }
803
804                    # alternative (optional)
805                    if ( -e "$Filename.content_alternative" ) {
806                        my $Content = $MainObject->FileRead(
807                            Location => "$Filename.content_alternative",
808                        );
809                        if ($Content) {
810                            $Data{Alternative} = ${$Content};
811                        }
812                    }
813
814                    # disposition
815                    if ( -e "$Filename.disposition" ) {
816                        my $Content = $MainObject->FileRead(
817                            Location => "$Filename.disposition",
818                        );
819                        if ($Content) {
820                            $Data{Disposition} = ${$Content};
821                        }
822                    }
823
824                    # if no content disposition is set images with content id should be inline
825                    elsif ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) {
826                        $Data{Disposition} = 'inline';
827                    }
828
829                    # converted article body should be inline
830                    elsif ( $Filename =~ m{file-[12]} ) {
831                        $Data{Disposition} = 'inline';
832                    }
833
834                    # all others including attachments with content id that are not images
835                    #   should NOT be inline
836                    else {
837                        $Data{Disposition} = 'attachment';
838                    }
839                }
840                else {
841
842                    # read content
843                    my $Content = $MainObject->FileRead(
844                        Location => $Filename,
845                        Mode     => 'binmode',
846                        Result   => 'ARRAY',
847                    );
848                    return if !$Content;
849                    $Data{ContentType} = $Content->[0];
850                    my $Counter = 0;
851                    for my $Line ( @{$Content} ) {
852                        if ($Counter) {
853                            $Data{Content} .= $Line;
854                        }
855                        $Counter++;
856                    }
857                }
858                if (
859                    $Data{ContentType} =~ /plain\/text/i
860                    && $Data{ContentType} =~ /(utf\-8|utf8)/i
861                    )
862                {
863                    $EncodeObject->EncodeInput( \$Data{Content} );
864                }
865
866                chomp $Data{ContentType};
867
868                # Write to special article storage cache.
869                if ( $Self->{ArticleStorageCache} ) {
870                    $CacheObject->Set(
871                        Type           => 'ArticleStorageFS_' . $Param{ArticleID},
872                        TTL            => $Self->{ArticleStorageCacheTTL},
873                        Key            => 'ArticleAttachment' . $Param{FileID},
874                        Value          => \%Data,
875                        CacheInMemory  => 0,
876                        CacheInBackend => 1,
877                    );
878                }
879
880                return %Data;
881            }
882        }
883    }
884
885    # return if we only need to check one backend
886    return if !$Self->{CheckAllBackends};
887
888    # return if only delete in my backend
889    return if $Param{OnlyMyBackend};
890
891    %Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleAttachment(
892        %Param,
893        OnlyMyBackend => 1,
894    );
895
896    # Write to special article storage cache.
897    if ( $Self->{ArticleStorageCache} ) {
898        $CacheObject->Set(
899            Type           => 'ArticleStorageFS_' . $Param{ArticleID},
900            TTL            => $Self->{ArticleStorageCacheTTL},
901            Key            => 'ArticleAttachment' . $Param{FileID},
902            Value          => \%Data,
903            CacheInMemory  => 0,
904            CacheInBackend => 1,
905        );
906    }
907
908    return %Data;
909}
910
9111;
912
913=head1 TERMS AND CONDITIONS
914
915This software is part of the OTRS project (L<https://otrs.org/>).
916
917This software comes with ABSOLUTELY NO WARRANTY. For details, see
918the enclosed file COPYING for license information (GPL). If you
919did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
920
921=cut
922