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