init III
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
# --
|
||||
# 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::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use MIME::Base64;
|
||||
use MIME::Words qw(:all);
|
||||
|
||||
use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base);
|
||||
|
||||
use Kernel::System::VariableCheck qw(:all);
|
||||
|
||||
our @ObjectDependencies = (
|
||||
'Kernel::Config',
|
||||
'Kernel::System::DB',
|
||||
'Kernel::System::DynamicFieldValue',
|
||||
'Kernel::System::Encode',
|
||||
'Kernel::System::Log',
|
||||
'Kernel::System::Main',
|
||||
'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS',
|
||||
);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB - DB based ticket article storage interface
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This class provides functions to manipulate ticket articles in the database.
|
||||
The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>.
|
||||
|
||||
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>.
|
||||
|
||||
See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS>.
|
||||
|
||||
=cut
|
||||
|
||||
sub ArticleDelete {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete attachments
|
||||
$Self->ArticleDeleteAttachment(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
# delete plain message
|
||||
$Self->ArticleDeletePlain(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
# Delete storage directory in case there are leftovers in the FS.
|
||||
$Self->_ArticleDeleteDirectory(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticleDeletePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete attachments
|
||||
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
|
||||
SQL => 'DELETE FROM article_data_mime_plain WHERE article_id = ?',
|
||||
Bind => [ \$Param{ArticleID} ],
|
||||
);
|
||||
|
||||
# return if we only need to check one backend
|
||||
return 1 if !$Self->{CheckAllBackends};
|
||||
|
||||
# return of only delete in my backend
|
||||
return 1 if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleDeletePlain(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleDeleteAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete attachments
|
||||
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
|
||||
SQL => 'DELETE FROM article_data_mime_attachment WHERE article_id = ?',
|
||||
Bind => [ \$Param{ArticleID} ],
|
||||
);
|
||||
|
||||
# return if we only need to check one backend
|
||||
return 1 if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return 1 if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')
|
||||
->ArticleDeleteAttachment(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleWritePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID Email UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# get database object
|
||||
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
||||
|
||||
# encode attachment if it's a postgresql backend!!!
|
||||
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
|
||||
|
||||
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Email} );
|
||||
|
||||
$Param{Email} = encode_base64( $Param{Email} );
|
||||
}
|
||||
|
||||
# write article to db 1:1
|
||||
return if !$DBObject->Do(
|
||||
SQL => 'INSERT INTO article_data_mime_plain '
|
||||
. ' (article_id, body, create_time, create_by, change_time, change_by) '
|
||||
. ' VALUES (?, ?, current_timestamp, ?, current_timestamp, ?)',
|
||||
Bind => [ \$Param{ArticleID}, \$Param{Email}, \$Param{UserID}, \$Param{UserID} ],
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticleWriteAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(Filename ContentType ArticleID UserID)) {
|
||||
if ( !IsStringWithData( $Param{$Item} ) ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$Param{Filename} = $Kernel::OM->Get('Kernel::System::Main')->FilenameCleanUp(
|
||||
Filename => $Param{Filename},
|
||||
Type => 'Local',
|
||||
NoReplace => 1,
|
||||
);
|
||||
|
||||
my $NewFileName = $Param{Filename};
|
||||
my %UsedFile;
|
||||
my %Index = $Self->ArticleAttachmentIndex(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
for my $IndexFile ( sort keys %Index ) {
|
||||
$UsedFile{ $Index{$IndexFile}->{Filename} } = 1;
|
||||
}
|
||||
for ( my $i = 1; $i <= 50; $i++ ) {
|
||||
if ( exists $UsedFile{$NewFileName} ) {
|
||||
if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) {
|
||||
$NewFileName = "$1-$i.$2";
|
||||
}
|
||||
else {
|
||||
$NewFileName = "$Param{Filename}-$i";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# get file name
|
||||
$Param{Filename} = $NewFileName;
|
||||
|
||||
# get attachment size
|
||||
$Param{Filesize} = bytes::length( $Param{Content} );
|
||||
|
||||
# get database object
|
||||
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
||||
|
||||
# encode attachment if it's a postgresql backend!!!
|
||||
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
|
||||
|
||||
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Content} );
|
||||
|
||||
$Param{Content} = encode_base64( $Param{Content} );
|
||||
}
|
||||
|
||||
# set content id in angle brackets
|
||||
if ( $Param{ContentID} ) {
|
||||
$Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/;
|
||||
}
|
||||
|
||||
my $Disposition;
|
||||
my $Filename;
|
||||
if ( $Param{Disposition} ) {
|
||||
( $Disposition, $Filename ) = split ';', $Param{Disposition};
|
||||
}
|
||||
$Disposition //= '';
|
||||
|
||||
# write attachment to db
|
||||
return if !$DBObject->Do(
|
||||
SQL => '
|
||||
INSERT INTO article_data_mime_attachment (article_id, filename, content_type, content_size,
|
||||
content, content_id, content_alternative, disposition, create_time, create_by,
|
||||
change_time, change_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
|
||||
Bind => [
|
||||
\$Param{ArticleID}, \$Param{Filename}, \$Param{ContentType}, \$Param{Filesize},
|
||||
\$Param{Content}, \$Param{ContentID}, \$Param{ContentAlternative},
|
||||
\$Disposition, \$Param{UserID}, \$Param{UserID},
|
||||
],
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticlePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
if ( !$Param{ArticleID} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need ArticleID!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
|
||||
# get database object
|
||||
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
||||
|
||||
# can't open article, try database
|
||||
return if !$DBObject->Prepare(
|
||||
SQL => 'SELECT body FROM article_data_mime_plain WHERE article_id = ?',
|
||||
Bind => [ \$Param{ArticleID} ],
|
||||
Encode => [0],
|
||||
);
|
||||
|
||||
my $Data;
|
||||
while ( my @Row = $DBObject->FetchrowArray() ) {
|
||||
|
||||
# decode attachment if it's e. g. a postgresql backend!!!
|
||||
if ( !$DBObject->GetDatabaseFunction('DirectBlob') && $Row[0] !~ m/ / ) {
|
||||
$Data = decode_base64( $Row[0] );
|
||||
}
|
||||
else {
|
||||
$Data = $Row[0];
|
||||
}
|
||||
}
|
||||
return $Data if defined $Data;
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return of only delete in my backend
|
||||
return if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticlePlain(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleAttachmentIndexRaw {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
if ( !$Param{ArticleID} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => 'Need ArticleID!'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
my %Index;
|
||||
my $Counter = 0;
|
||||
|
||||
# get database object
|
||||
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
||||
|
||||
# try database
|
||||
return if !$DBObject->Prepare(
|
||||
SQL => '
|
||||
SELECT filename, content_type, content_size, content_id, content_alternative,
|
||||
disposition
|
||||
FROM article_data_mime_attachment
|
||||
WHERE article_id = ?
|
||||
ORDER BY filename, id',
|
||||
Bind => [ \$Param{ArticleID} ],
|
||||
);
|
||||
|
||||
while ( my @Row = $DBObject->FetchrowArray() ) {
|
||||
|
||||
my $Disposition = $Row[5];
|
||||
if ( !$Disposition ) {
|
||||
|
||||
# if no content disposition is set images with content id should be inline
|
||||
if ( $Row[3] && $Row[1] =~ m{image}i ) {
|
||||
$Disposition = 'inline';
|
||||
}
|
||||
|
||||
# converted article body should be inline
|
||||
elsif ( $Row[0] =~ m{file-[12]} ) {
|
||||
$Disposition = 'inline';
|
||||
}
|
||||
|
||||
# all others including attachments with content id that are not images
|
||||
# should NOT be inline
|
||||
else {
|
||||
$Disposition = 'attachment';
|
||||
}
|
||||
}
|
||||
|
||||
# add the info the the hash
|
||||
$Counter++;
|
||||
$Index{$Counter} = {
|
||||
Filename => $Row[0],
|
||||
FilesizeRaw => $Row[2] || 0,
|
||||
ContentType => $Row[1],
|
||||
ContentID => $Row[3] || '',
|
||||
ContentAlternative => $Row[4] || '',
|
||||
Disposition => $Disposition,
|
||||
};
|
||||
}
|
||||
|
||||
# return existing index
|
||||
return %Index if %Index;
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')
|
||||
->ArticleAttachmentIndexRaw(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID FileID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
|
||||
# get attachment index
|
||||
my %Index = $Self->ArticleAttachmentIndex(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
return if !$Index{ $Param{FileID} };
|
||||
my %Data = %{ $Index{ $Param{FileID} } };
|
||||
|
||||
# get database object
|
||||
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
||||
|
||||
# try database
|
||||
return if !$DBObject->Prepare(
|
||||
SQL => '
|
||||
SELECT id
|
||||
FROM article_data_mime_attachment
|
||||
WHERE article_id = ?
|
||||
ORDER BY filename, id',
|
||||
Bind => [ \$Param{ArticleID} ],
|
||||
Limit => $Param{FileID},
|
||||
);
|
||||
|
||||
my $AttachmentID;
|
||||
while ( my @Row = $DBObject->FetchrowArray() ) {
|
||||
$AttachmentID = $Row[0];
|
||||
}
|
||||
|
||||
return if !$DBObject->Prepare(
|
||||
SQL => '
|
||||
SELECT content_type, content, content_id, content_alternative, disposition, filename
|
||||
FROM article_data_mime_attachment
|
||||
WHERE id = ?',
|
||||
Bind => [ \$AttachmentID ],
|
||||
Encode => [ 1, 0, 0, 0, 1, 1 ],
|
||||
);
|
||||
|
||||
while ( my @Row = $DBObject->FetchrowArray() ) {
|
||||
|
||||
$Data{ContentType} = $Row[0];
|
||||
|
||||
# decode attachment if it's e. g. a postgresql backend!!!
|
||||
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
|
||||
$Data{Content} = decode_base64( $Row[1] );
|
||||
}
|
||||
else {
|
||||
$Data{Content} = $Row[1];
|
||||
}
|
||||
$Data{ContentID} = $Row[2] || '';
|
||||
$Data{ContentAlternative} = $Row[3] || '';
|
||||
$Data{Disposition} = $Row[4];
|
||||
$Data{Filename} = $Row[5];
|
||||
}
|
||||
|
||||
if ( !$Data{Disposition} ) {
|
||||
|
||||
# if no content disposition is set images with content id should be inline
|
||||
if ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) {
|
||||
$Data{Disposition} = 'inline';
|
||||
}
|
||||
|
||||
# converted article body should be inline
|
||||
elsif ( $Data{Filename} =~ m{file-[12]} ) {
|
||||
$Data{Disposition} = 'inline';
|
||||
}
|
||||
|
||||
# all others including attachments with content id that are not images
|
||||
# should NOT be inline
|
||||
else {
|
||||
$Data{Disposition} = 'attachment';
|
||||
}
|
||||
}
|
||||
|
||||
return %Data if defined $Data{Content};
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleAttachment(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,921 @@
|
||||
# --
|
||||
# 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::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Path qw();
|
||||
use MIME::Base64 qw();
|
||||
use Time::HiRes qw();
|
||||
use Unicode::Normalize qw();
|
||||
|
||||
use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base);
|
||||
|
||||
use Kernel::System::VariableCheck qw(:all);
|
||||
|
||||
our @ObjectDependencies = (
|
||||
'Kernel::Config',
|
||||
'Kernel::System::Cache',
|
||||
'Kernel::System::DB',
|
||||
'Kernel::System::DynamicFieldValue',
|
||||
'Kernel::System::Encode',
|
||||
'Kernel::System::Log',
|
||||
'Kernel::System::Main',
|
||||
'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB',
|
||||
);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS - FS based ticket article storage interface
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This class provides functions to manipulate ticket articles on the file system.
|
||||
The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>.
|
||||
|
||||
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>.
|
||||
|
||||
See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB>.
|
||||
|
||||
=cut
|
||||
|
||||
sub new {
|
||||
my ( $Type, %Param ) = @_;
|
||||
|
||||
# Call new() on Base.pm to execute the common code.
|
||||
my $Self = $Type->SUPER::new(%Param);
|
||||
|
||||
my $ArticleContentPath = $Self->BuildArticleContentPath();
|
||||
my $ArticleDir = "$Self->{ArticleDataDir}/$ArticleContentPath/";
|
||||
|
||||
File::Path::mkpath( $ArticleDir, 0, 0770 ); ## no critic
|
||||
|
||||
# Check write permissions.
|
||||
if ( !-w $ArticleDir ) {
|
||||
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'notice',
|
||||
Message => "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!",
|
||||
);
|
||||
die "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!";
|
||||
}
|
||||
|
||||
# Get activated cache backend configuration.
|
||||
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
|
||||
return $Self if !$ConfigObject->Get('Cache::ArticleStorageCache');
|
||||
|
||||
my $CacheModule = $ConfigObject->Get('Cache::Module') || '';
|
||||
return $Self if $CacheModule ne 'Kernel::System::Cache::MemcachedFast';
|
||||
|
||||
# Turn on special cache used for speeding up article storage methods in huge systems with many
|
||||
# nodes and slow FS access. It will be used only in environments with configured Memcached
|
||||
# backend (see config above).
|
||||
$Self->{ArticleStorageCache} = 1;
|
||||
$Self->{ArticleStorageCacheTTL} = $ConfigObject->Get('Cache::ArticleStorageCache::TTL') || 60 * 60 * 24;
|
||||
|
||||
return $Self;
|
||||
}
|
||||
|
||||
sub ArticleDelete {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete attachments
|
||||
$Self->ArticleDeleteAttachment(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
# delete plain message
|
||||
$Self->ArticleDeletePlain(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
# delete storage directory
|
||||
$Self->_ArticleDeleteDirectory(
|
||||
ArticleID => $Param{ArticleID},
|
||||
UserID => $Param{UserID},
|
||||
);
|
||||
|
||||
# Delete special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticleDeletePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete from fs
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
my $File = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt";
|
||||
if ( -f $File ) {
|
||||
if ( !unlink $File ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Can't remove: $File: $!!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# Delete special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Cache')->Delete(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
Key => 'ArticlePlain',
|
||||
);
|
||||
}
|
||||
|
||||
# return if only delete in my backend
|
||||
return 1 if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleDeletePlain(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleDeleteAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# delete from fs
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
|
||||
|
||||
if ( -e $Path ) {
|
||||
|
||||
my @List = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
|
||||
Directory => $Path,
|
||||
Filter => "*",
|
||||
);
|
||||
|
||||
for my $File (@List) {
|
||||
|
||||
if ( $File !~ /(\/|\\)plain.txt$/ ) {
|
||||
|
||||
if ( !unlink "$File" ) {
|
||||
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Can't remove: $File: $!!",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Delete special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
);
|
||||
}
|
||||
|
||||
# return if only delete in my backend
|
||||
return 1 if $Param{OnlyMyBackend};
|
||||
|
||||
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
|
||||
->ArticleDeleteAttachment(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
sub ArticleWritePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID Email UserID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
|
||||
# define path
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
my $Path = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
|
||||
|
||||
# debug
|
||||
if ( defined $Self->{Debug} && $Self->{Debug} > 1 ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log( Message => "->WriteArticle: $Path" );
|
||||
}
|
||||
|
||||
# write article to fs 1:1
|
||||
File::Path::mkpath( [$Path], 0, 0770 ); ## no critic
|
||||
|
||||
# write article to fs
|
||||
my $Success = $Kernel::OM->Get('Kernel::System::Main')->FileWrite(
|
||||
Location => "$Path/plain.txt",
|
||||
Mode => 'binmode',
|
||||
Content => \$Param{Email},
|
||||
Permission => '660',
|
||||
);
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Cache')->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticlePlain',
|
||||
Value => $Param{Email},
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return if !$Success;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticleWriteAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(Filename ContentType ArticleID UserID)) {
|
||||
if ( !IsStringWithData( $Param{$Item} ) ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
# define path
|
||||
$Param{Path} = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
|
||||
|
||||
# get main object
|
||||
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
||||
|
||||
# Perform FilenameCleanup here already to check for
|
||||
# conflicting existing attachment files correctly
|
||||
$Param{Filename} = $MainObject->FilenameCleanUp(
|
||||
Filename => $Param{Filename},
|
||||
Type => 'Local',
|
||||
);
|
||||
|
||||
my $NewFileName = $Param{Filename};
|
||||
my %UsedFile;
|
||||
my %Index = $Self->ArticleAttachmentIndex(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
# Normalize filenames to find file names which are identical but in a different unicode form.
|
||||
# This is needed because Mac OS (HFS+) converts all filenames to NFD internally.
|
||||
# Without this, the same file might be overwritten because the strings are not equal.
|
||||
for my $Position ( sort keys %Index ) {
|
||||
$UsedFile{ Unicode::Normalize::NFC( $Index{$Position}->{Filename} ) } = 1;
|
||||
}
|
||||
for ( my $i = 1; $i <= 50; $i++ ) {
|
||||
if ( exists $UsedFile{ Unicode::Normalize::NFC($NewFileName) } ) {
|
||||
if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) {
|
||||
$NewFileName = "$1-$i.$2";
|
||||
}
|
||||
else {
|
||||
$NewFileName = "$Param{Filename}-$i";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$Param{Filename} = $NewFileName;
|
||||
|
||||
# write attachment to backend
|
||||
if ( !-d $Param{Path} ) {
|
||||
if ( !File::Path::mkpath( [ $Param{Path} ], 0, 0770 ) ) { ## no critic
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Can't create $Param{Path}: $!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# write attachment content type to fs
|
||||
my $SuccessContentType = $MainObject->FileWrite(
|
||||
Directory => $Param{Path},
|
||||
Filename => "$Param{Filename}.content_type",
|
||||
Mode => 'binmode',
|
||||
Content => \$Param{ContentType},
|
||||
Permission => 660,
|
||||
NoFilenameClean => 1,
|
||||
);
|
||||
return if !$SuccessContentType;
|
||||
|
||||
# set content id in angle brackets
|
||||
if ( $Param{ContentID} ) {
|
||||
$Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/;
|
||||
}
|
||||
|
||||
# write attachment content id to fs
|
||||
if ( $Param{ContentID} ) {
|
||||
$MainObject->FileWrite(
|
||||
Directory => $Param{Path},
|
||||
Filename => "$Param{Filename}.content_id",
|
||||
Mode => 'binmode',
|
||||
Content => \$Param{ContentID},
|
||||
Permission => 660,
|
||||
NoFilenameClean => 1,
|
||||
);
|
||||
}
|
||||
|
||||
# write attachment content alternative to fs
|
||||
if ( $Param{ContentAlternative} ) {
|
||||
$MainObject->FileWrite(
|
||||
Directory => $Param{Path},
|
||||
Filename => "$Param{Filename}.content_alternative",
|
||||
Mode => 'binmode',
|
||||
Content => \$Param{ContentAlternative},
|
||||
Permission => 660,
|
||||
NoFilenameClean => 1,
|
||||
);
|
||||
}
|
||||
|
||||
# write attachment disposition to fs
|
||||
if ( $Param{Disposition} ) {
|
||||
|
||||
my ( $Disposition, $FileName ) = split ';', $Param{Disposition};
|
||||
|
||||
$MainObject->FileWrite(
|
||||
Directory => $Param{Path},
|
||||
Filename => "$Param{Filename}.disposition",
|
||||
Mode => 'binmode',
|
||||
Content => \$Disposition || '',
|
||||
Permission => 660,
|
||||
NoFilenameClean => 1,
|
||||
);
|
||||
}
|
||||
|
||||
# write attachment content to fs
|
||||
my $SuccessContent = $MainObject->FileWrite(
|
||||
Directory => $Param{Path},
|
||||
Filename => $Param{Filename},
|
||||
Mode => 'binmode',
|
||||
Content => \$Param{Content},
|
||||
Permission => 660,
|
||||
);
|
||||
|
||||
return if !$SuccessContent;
|
||||
|
||||
# Delete special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub ArticlePlain {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
if ( !$Param{ArticleID} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => 'Need ArticleID!',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
|
||||
# get cache object
|
||||
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
||||
|
||||
# Read from special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
my $Cache = $CacheObject->Get(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
Key => 'ArticlePlain',
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
|
||||
return $Cache if $Cache;
|
||||
}
|
||||
|
||||
# get content path
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
# open plain article
|
||||
if ( -f "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt" ) {
|
||||
|
||||
# read whole article
|
||||
my $Data = $Kernel::OM->Get('Kernel::System::Main')->FileRead(
|
||||
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/",
|
||||
Filename => 'plain.txt',
|
||||
Mode => 'binmode',
|
||||
);
|
||||
|
||||
return if !$Data;
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticlePlain',
|
||||
Value => ${$Data},
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return ${$Data};
|
||||
}
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return if $Param{OnlyMyBackend};
|
||||
|
||||
my $Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticlePlain(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticlePlain',
|
||||
Value => $Data,
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return $Data;
|
||||
}
|
||||
|
||||
sub ArticleAttachmentIndexRaw {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
if ( !$Param{ArticleID} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => 'Need ArticleID!',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
# get cache object
|
||||
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
||||
|
||||
# Read from special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
my $Cache = $CacheObject->Get(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
Key => 'ArticleAttachmentIndexRaw',
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
|
||||
return %{$Cache} if $Cache;
|
||||
}
|
||||
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
my %Index;
|
||||
my $Counter = 0;
|
||||
|
||||
# get main object
|
||||
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
||||
|
||||
# try fs
|
||||
my @List = $MainObject->DirectoryRead(
|
||||
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
|
||||
Filter => "*",
|
||||
Silent => 1,
|
||||
);
|
||||
|
||||
FILENAME:
|
||||
for my $Filename ( sort @List ) {
|
||||
my $FileSizeRaw = -s $Filename;
|
||||
|
||||
# do not use control file
|
||||
next FILENAME if $Filename =~ /\.content_alternative$/;
|
||||
next FILENAME if $Filename =~ /\.content_id$/;
|
||||
next FILENAME if $Filename =~ /\.content_type$/;
|
||||
next FILENAME if $Filename =~ /\.disposition$/;
|
||||
next FILENAME if $Filename =~ /\/plain.txt$/;
|
||||
|
||||
# read content type
|
||||
my $ContentType = '';
|
||||
my $ContentID = '';
|
||||
my $Alternative = '';
|
||||
my $Disposition = '';
|
||||
if ( -e "$Filename.content_type" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_type",
|
||||
);
|
||||
return if !$Content;
|
||||
$ContentType = ${$Content};
|
||||
|
||||
# content id (optional)
|
||||
if ( -e "$Filename.content_id" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_id",
|
||||
);
|
||||
if ($Content) {
|
||||
$ContentID = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# alternative (optional)
|
||||
if ( -e "$Filename.content_alternative" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_alternative",
|
||||
);
|
||||
if ($Content) {
|
||||
$Alternative = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# disposition
|
||||
if ( -e "$Filename.disposition" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.disposition",
|
||||
);
|
||||
if ($Content) {
|
||||
$Disposition = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# if no content disposition is set images with content id should be inline
|
||||
elsif ( $ContentID && $ContentType =~ m{image}i ) {
|
||||
$Disposition = 'inline';
|
||||
}
|
||||
|
||||
# converted article body should be inline
|
||||
elsif ( $Filename =~ m{file-[12]} ) {
|
||||
$Disposition = 'inline';
|
||||
}
|
||||
|
||||
# all others including attachments with content id that are not images
|
||||
# should NOT be inline
|
||||
else {
|
||||
$Disposition = 'attachment';
|
||||
}
|
||||
}
|
||||
|
||||
# read content type (old style)
|
||||
else {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => $Filename,
|
||||
Result => 'ARRAY',
|
||||
);
|
||||
if ( !$Content ) {
|
||||
return;
|
||||
}
|
||||
$ContentType = $Content->[0];
|
||||
}
|
||||
|
||||
# strip filename
|
||||
$Filename =~ s!^.*/!!;
|
||||
|
||||
# add the info the the hash
|
||||
$Counter++;
|
||||
$Index{$Counter} = {
|
||||
Filename => $Filename,
|
||||
FilesizeRaw => $FileSizeRaw,
|
||||
ContentType => $ContentType,
|
||||
ContentID => $ContentID,
|
||||
ContentAlternative => $Alternative,
|
||||
Disposition => $Disposition,
|
||||
};
|
||||
}
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticleAttachmentIndexRaw',
|
||||
Value => \%Index,
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return %Index if %Index;
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return %Index if $Param{OnlyMyBackend};
|
||||
|
||||
%Index = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
|
||||
->ArticleAttachmentIndexRaw(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticleAttachmentIndexRaw',
|
||||
Value => \%Index,
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return %Index;
|
||||
}
|
||||
|
||||
sub ArticleAttachment {
|
||||
my ( $Self, %Param ) = @_;
|
||||
|
||||
# check needed stuff
|
||||
for my $Item (qw(ArticleID FileID)) {
|
||||
if ( !$Param{$Item} ) {
|
||||
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
||||
Priority => 'error',
|
||||
Message => "Need $Item!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# prepare/filter ArticleID
|
||||
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
|
||||
$Param{ArticleID} =~ s/\0//g;
|
||||
|
||||
# get cache object
|
||||
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
||||
|
||||
# Read from special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
my $Cache = $CacheObject->Get(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
Key => 'ArticleAttachment' . $Param{FileID},
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
|
||||
return %{$Cache} if $Cache;
|
||||
}
|
||||
|
||||
# get attachment index
|
||||
my %Index = $Self->ArticleAttachmentIndex(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
|
||||
# get content path
|
||||
my $ContentPath = $Self->_ArticleContentPathGet(
|
||||
ArticleID => $Param{ArticleID},
|
||||
);
|
||||
my %Data = %{ $Index{ $Param{FileID} } // {} };
|
||||
my $Counter = 0;
|
||||
|
||||
# get main object
|
||||
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
||||
|
||||
my @List = $MainObject->DirectoryRead(
|
||||
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
|
||||
Filter => "*",
|
||||
Silent => 1,
|
||||
);
|
||||
|
||||
if (@List) {
|
||||
|
||||
# get encode object
|
||||
my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');
|
||||
|
||||
FILENAME:
|
||||
for my $Filename (@List) {
|
||||
next FILENAME if $Filename =~ /\.content_alternative$/;
|
||||
next FILENAME if $Filename =~ /\.content_id$/;
|
||||
next FILENAME if $Filename =~ /\.content_type$/;
|
||||
next FILENAME if $Filename =~ /\/plain.txt$/;
|
||||
next FILENAME if $Filename =~ /\.disposition$/;
|
||||
|
||||
# add the info the the hash
|
||||
$Counter++;
|
||||
if ( $Counter == $Param{FileID} ) {
|
||||
|
||||
if ( -e "$Filename.content_type" ) {
|
||||
|
||||
# read content type
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_type",
|
||||
);
|
||||
return if !$Content;
|
||||
$Data{ContentType} = ${$Content};
|
||||
|
||||
# read content
|
||||
$Content = $MainObject->FileRead(
|
||||
Location => $Filename,
|
||||
Mode => 'binmode',
|
||||
);
|
||||
return if !$Content;
|
||||
$Data{Content} = ${$Content};
|
||||
|
||||
# content id (optional)
|
||||
if ( -e "$Filename.content_id" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_id",
|
||||
);
|
||||
if ($Content) {
|
||||
$Data{ContentID} = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# alternative (optional)
|
||||
if ( -e "$Filename.content_alternative" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.content_alternative",
|
||||
);
|
||||
if ($Content) {
|
||||
$Data{Alternative} = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# disposition
|
||||
if ( -e "$Filename.disposition" ) {
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => "$Filename.disposition",
|
||||
);
|
||||
if ($Content) {
|
||||
$Data{Disposition} = ${$Content};
|
||||
}
|
||||
}
|
||||
|
||||
# if no content disposition is set images with content id should be inline
|
||||
elsif ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) {
|
||||
$Data{Disposition} = 'inline';
|
||||
}
|
||||
|
||||
# converted article body should be inline
|
||||
elsif ( $Filename =~ m{file-[12]} ) {
|
||||
$Data{Disposition} = 'inline';
|
||||
}
|
||||
|
||||
# all others including attachments with content id that are not images
|
||||
# should NOT be inline
|
||||
else {
|
||||
$Data{Disposition} = 'attachment';
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
# read content
|
||||
my $Content = $MainObject->FileRead(
|
||||
Location => $Filename,
|
||||
Mode => 'binmode',
|
||||
Result => 'ARRAY',
|
||||
);
|
||||
return if !$Content;
|
||||
$Data{ContentType} = $Content->[0];
|
||||
my $Counter = 0;
|
||||
for my $Line ( @{$Content} ) {
|
||||
if ($Counter) {
|
||||
$Data{Content} .= $Line;
|
||||
}
|
||||
$Counter++;
|
||||
}
|
||||
}
|
||||
if (
|
||||
$Data{ContentType} =~ /plain\/text/i
|
||||
&& $Data{ContentType} =~ /(utf\-8|utf8)/i
|
||||
)
|
||||
{
|
||||
$EncodeObject->EncodeInput( \$Data{Content} );
|
||||
}
|
||||
|
||||
chomp $Data{ContentType};
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticleAttachment' . $Param{FileID},
|
||||
Value => \%Data,
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return %Data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# return if we only need to check one backend
|
||||
return if !$Self->{CheckAllBackends};
|
||||
|
||||
# return if only delete in my backend
|
||||
return if $Param{OnlyMyBackend};
|
||||
|
||||
%Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleAttachment(
|
||||
%Param,
|
||||
OnlyMyBackend => 1,
|
||||
);
|
||||
|
||||
# Write to special article storage cache.
|
||||
if ( $Self->{ArticleStorageCache} ) {
|
||||
$CacheObject->Set(
|
||||
Type => 'ArticleStorageFS_' . $Param{ArticleID},
|
||||
TTL => $Self->{ArticleStorageCacheTTL},
|
||||
Key => 'ArticleAttachment' . $Param{FileID},
|
||||
Value => \%Data,
|
||||
CacheInMemory => 0,
|
||||
CacheInBackend => 1,
|
||||
);
|
||||
}
|
||||
|
||||
return %Data;
|
||||
}
|
||||
|
||||
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
|
||||
357
Perl OTRS/Kernel/System/Ticket/Article/Backend/MIMEBase/Base.pm
Normal file
357
Perl OTRS/Kernel/System/Ticket/Article/Backend/MIMEBase/Base.pm
Normal file
@@ -0,0 +1,357 @@
|
||||
# --
|
||||
# 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::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->_ArticleContentPatGeth(
|
||||
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
|
||||
Reference in New Issue
Block a user