# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.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 https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::Ticket::Article::Backend::MIMEBase::Base;

use strict;
use warnings;

our $ObjectManagerDisabled = 1;

=head1 NAME

Kernel::System::Ticket::Article::Backend::MIMEBase::Base - base class for article storage modules

=head1 DESCRIPTION

This is a base class for article storage backends and should not be instantiated directly.

=head1 PUBLIC INTERFACE

=cut

=head2 new()

Don't instantiate this class directly, get instances of the real storage backends instead:

    my $BackendObject = $Kernel::OM->Get('Kernel::System::Article::Backend::MIMEBase::ArticleStorageDB');

=cut

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {};
    bless( $Self, $Type );

    $Self->{CacheType} = 'ArticleStorageBase';
    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;

    $Self->{ArticleDataDir}
        = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir')
        || die 'Got no ArticleDataDir!';

    # do we need to check all backends, or just one?
    $Self->{CheckAllBackends}
        = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::CheckAllStorageBackends')
        // 0;

    return $Self;
}

=head2 BuildArticleContentPath()

Generate a base article content path for article storage in the file system.

    my $ArticleContentPath = $BackendObject->BuildArticleContentPath();

=cut

sub BuildArticleContentPath {
    my ( $Self, %Param ) = @_;

    return $Self->{ArticleContentPath} if $Self->{ArticleContentPath};

    $Self->{ArticleContentPath} = $Kernel::OM->Create('Kernel::System::DateTime')->Format(
        Format => '%Y/%m/%d',
    );

    return $Self->{ArticleContentPath};
}

=head2 ArticleAttachmentIndex()

Get article attachment index as hash.

    my %Index = $BackendObject->ArticleAttachmentIndex(
        ArticleID        => 123,
        ExcludePlainText => 1,       # (optional) Exclude plain text attachment
        ExcludeHTMLBody  => 1,       # (optional) Exclude HTML body attachment
        ExcludeInline    => 1,       # (optional) Exclude inline attachments
        OnlyHTMLBody     => 1,       # (optional) Return only HTML body attachment, return nothing if not found
    );

Returns:

    my %Index = {
        '1' => {                                                # Attachment ID
            ContentAlternative => '',                           # (optional)
            ContentID          => '',                           # (optional)
            ContentType        => 'application/pdf',
            Filename           => 'StdAttachment-Test1.pdf',
            FilesizeRaw        => 4722,
            Disposition        => 'attachment',
        },
        '2' => {
            ContentAlternative => '',
            ContentID          => '',
            ContentType        => 'text/html; charset="utf-8"',
            Filename           => 'file-2',
            FilesizeRaw        => 183,
            Disposition        => 'attachment',
        },
        ...
    };

=cut

sub ArticleAttachmentIndex {
    my ( $Self, %Param ) = @_;

    if ( !$Param{ArticleID} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need ArticleID!',
        );
        return;
    }

    if ( $Param{ExcludeHTMLBody} && $Param{OnlyHTMLBody} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'ExcludeHTMLBody and OnlyHTMLBody cannot be used together!',
        );
        return;
    }

    # Get complete attachment index from backend.
    my %Attachments = $Self->ArticleAttachmentIndexRaw(%Param);

    # Iterate over attachments only if any of optional parameters is active.
    if ( $Param{ExcludePlainText} || $Param{ExcludeHTMLBody} || $Param{ExcludeInline} || $Param{OnlyHTMLBody} ) {

        my $AttachmentIDPlain = 0;
        my $AttachmentIDHTML  = 0;

        ATTACHMENT_ID:
        for my $AttachmentID ( sort keys %Attachments ) {
            my %File = %{ $Attachments{$AttachmentID} };

            # Identify plain text attachment.
            if (
                !$AttachmentIDPlain
                &&
                $File{Filename} eq 'file-1'
                && $File{ContentType} =~ /text\/plain/i
                && $File{Disposition} eq 'inline'
                )
            {
                $AttachmentIDPlain = $AttachmentID;
                next ATTACHMENT_ID;
            }

            # Identify html body attachment:
            #   - file-[12] is plain+html attachment
            #   - file-1.html is html attachment only
            if (
                !$AttachmentIDHTML
                &&
                ( $File{Filename} =~ /^file-[12]$/ || $File{Filename} eq 'file-1.html' )
                && $File{ContentType} =~ /text\/html/i
                && $File{Disposition} eq 'inline'
                )
            {
                $AttachmentIDHTML = $AttachmentID;
                next ATTACHMENT_ID;
            }
        }

        # If neither plain text or html body were found, iterate again to try to identify plain text among regular
        #   non-inline attachments.
        if ( !$AttachmentIDPlain && !$AttachmentIDHTML ) {
            ATTACHMENT_ID:
            for my $AttachmentID ( sort keys %Attachments ) {
                my %File = %{ $Attachments{$AttachmentID} };

                # Remember, file-1 got defined by parsing if no filename was given.
                if (
                    $File{Filename} eq 'file-1'
                    && $File{ContentType} =~ /text\/plain/i
                    )
                {
                    $AttachmentIDPlain = $AttachmentID;
                    last ATTACHMENT_ID;
                }
            }
        }

        # Identify inline (image) attachments which are referenced in HTML body. Do not strip attachments based on their
        #   disposition, since this method of detection is unreliable. Please see bug#13353 for more information.
        my @AttachmentIDsInline;

        if ($AttachmentIDHTML) {

            # Get HTML article body.
            my %HTMLBody = $Self->ArticleAttachment(
                ArticleID => $Param{ArticleID},
                FileID    => $AttachmentIDHTML,
            );

            if ( %HTMLBody && $HTMLBody{Content} ) {

                ATTACHMENT_ID:
                for my $AttachmentID ( sort keys %Attachments ) {
                    my %File = %{ $Attachments{$AttachmentID} };

                    next ATTACHMENT_ID if $File{ContentType} !~ m{image}ixms;
                    next ATTACHMENT_ID if !$File{ContentID};

                    my ($ImageID) = ( $File{ContentID} =~ m{^<(.*)>$}ixms );

                    # Search in the article body if there is any reference to it.
                    if ( $HTMLBody{Content} =~ m{<img.+src=['|"]cid:\Q$ImageID\E['|"].*>}ixms ) {
                        push @AttachmentIDsInline, $AttachmentID;
                    }
                }
            }
        }

        if ( $AttachmentIDPlain && $Param{ExcludePlainText} ) {
            delete $Attachments{$AttachmentIDPlain};
        }

        if ( $AttachmentIDHTML && $Param{ExcludeHTMLBody} ) {
            delete $Attachments{$AttachmentIDHTML};
        }

        if ( $Param{ExcludeInline} ) {
            for my $AttachmentID (@AttachmentIDsInline) {
                delete $Attachments{$AttachmentID};
            }
        }

        if ( $Param{OnlyHTMLBody} ) {
            if ($AttachmentIDHTML) {
                %Attachments = (
                    $AttachmentIDHTML => $Attachments{$AttachmentIDHTML}
                );
            }
            else {
                %Attachments = ();
            }
        }
    }

    return %Attachments;
}

=head1 PRIVATE FUNCTIONS

=cut

sub _ArticleDeleteDirectory {
    my ( $Self, %Param ) = @_;

    for my $Needed (qw(ArticleID UserID)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # delete directory from fs
    my $ContentPath = $Self->_ArticleContentPathGet(
        ArticleID => $Param{ArticleID},
    );
    my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
    if ( -d $Path ) {
        if ( !rmdir $Path ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Can't remove '$Path': $!.",
            );
            return;
        }
    }
    return 1;
}

=head2 _ArticleContentPathGet()

Get the stored content path of an article.

    my $Path = $BackendObject->_ArticleContentPathGet(
        ArticleID => 123,
    );

=cut

sub _ArticleContentPathGet {
    my ( $Self, %Param ) = @_;

    if ( !$Param{ArticleID} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need ArticleID!',
        );
        return;
    }

    # check key
    my $CacheKey = '_ArticleContentPathGet::' . $Param{ArticleID};

    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');

    # check cache
    my $Cache = $CacheObject->Get(
        Type => $Self->{CacheType},
        Key  => $CacheKey,
    );
    return $Cache if $Cache;

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # sql query
    return if !$DBObject->Prepare(
        SQL  => 'SELECT content_path FROM article_data_mime WHERE article_id = ?',
        Bind => [ \$Param{ArticleID} ],
    );

    my $Result;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Result = $Row[0];
    }

    # set cache
    $CacheObject->Set(
        Type  => $Self->{CacheType},
        TTL   => $Self->{CacheTTL},
        Key   => $CacheKey,
        Value => $Result,
    );

    # return
    return $Result;
}

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
