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::ArticleStorageDB; 10 11use strict; 12use warnings; 13 14use MIME::Base64; 15use MIME::Words qw(:all); 16 17use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base); 18 19use Kernel::System::VariableCheck qw(:all); 20 21our @ObjectDependencies = ( 22 'Kernel::Config', 23 'Kernel::System::DB', 24 'Kernel::System::DynamicFieldValue', 25 'Kernel::System::Encode', 26 'Kernel::System::Log', 27 'Kernel::System::Main', 28 'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS', 29); 30 31=head1 NAME 32 33Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB - DB based ticket article storage interface 34 35=head1 DESCRIPTION 36 37This class provides functions to manipulate ticket articles in the database. 38The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>. 39 40Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>. 41 42See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS>. 43 44=cut 45 46sub ArticleDelete { 47 my ( $Self, %Param ) = @_; 48 49 # check needed stuff 50 for my $Item (qw(ArticleID UserID)) { 51 if ( !$Param{$Item} ) { 52 $Kernel::OM->Get('Kernel::System::Log')->Log( 53 Priority => 'error', 54 Message => "Need $Item!" 55 ); 56 return; 57 } 58 } 59 60 # delete attachments 61 $Self->ArticleDeleteAttachment( 62 ArticleID => $Param{ArticleID}, 63 UserID => $Param{UserID}, 64 ); 65 66 # delete plain message 67 $Self->ArticleDeletePlain( 68 ArticleID => $Param{ArticleID}, 69 UserID => $Param{UserID}, 70 ); 71 72 # Delete storage directory in case there are leftovers in the FS. 73 $Self->_ArticleDeleteDirectory( 74 ArticleID => $Param{ArticleID}, 75 UserID => $Param{UserID}, 76 ); 77 78 return 1; 79} 80 81sub ArticleDeletePlain { 82 my ( $Self, %Param ) = @_; 83 84 # check needed stuff 85 for my $Item (qw(ArticleID UserID)) { 86 if ( !$Param{$Item} ) { 87 $Kernel::OM->Get('Kernel::System::Log')->Log( 88 Priority => 'error', 89 Message => "Need $Item!" 90 ); 91 return; 92 } 93 } 94 95 # delete attachments 96 return if !$Kernel::OM->Get('Kernel::System::DB')->Do( 97 SQL => 'DELETE FROM article_data_mime_plain WHERE article_id = ?', 98 Bind => [ \$Param{ArticleID} ], 99 ); 100 101 # return if we only need to check one backend 102 return 1 if !$Self->{CheckAllBackends}; 103 104 # return of only delete in my backend 105 return 1 if $Param{OnlyMyBackend}; 106 107 return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleDeletePlain( 108 %Param, 109 OnlyMyBackend => 1, 110 ); 111} 112 113sub ArticleDeleteAttachment { 114 my ( $Self, %Param ) = @_; 115 116 # check needed stuff 117 for my $Item (qw(ArticleID UserID)) { 118 if ( !$Param{$Item} ) { 119 $Kernel::OM->Get('Kernel::System::Log')->Log( 120 Priority => 'error', 121 Message => "Need $Item!" 122 ); 123 return; 124 } 125 } 126 127 # delete attachments 128 return if !$Kernel::OM->Get('Kernel::System::DB')->Do( 129 SQL => 'DELETE FROM article_data_mime_attachment WHERE article_id = ?', 130 Bind => [ \$Param{ArticleID} ], 131 ); 132 133 # return if we only need to check one backend 134 return 1 if !$Self->{CheckAllBackends}; 135 136 # return if only delete in my backend 137 return 1 if $Param{OnlyMyBackend}; 138 139 return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS') 140 ->ArticleDeleteAttachment( 141 %Param, 142 OnlyMyBackend => 1, 143 ); 144} 145 146sub ArticleWritePlain { 147 my ( $Self, %Param ) = @_; 148 149 # check needed stuff 150 for my $Item (qw(ArticleID Email UserID)) { 151 if ( !$Param{$Item} ) { 152 $Kernel::OM->Get('Kernel::System::Log')->Log( 153 Priority => 'error', 154 Message => "Need $Item!" 155 ); 156 return; 157 } 158 } 159 160 # get database object 161 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 162 163 # encode attachment if it's a postgresql backend!!! 164 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 165 166 $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Email} ); 167 168 $Param{Email} = encode_base64( $Param{Email} ); 169 } 170 171 # write article to db 1:1 172 return if !$DBObject->Do( 173 SQL => 'INSERT INTO article_data_mime_plain ' 174 . ' (article_id, body, create_time, create_by, change_time, change_by) ' 175 . ' VALUES (?, ?, current_timestamp, ?, current_timestamp, ?)', 176 Bind => [ \$Param{ArticleID}, \$Param{Email}, \$Param{UserID}, \$Param{UserID} ], 177 ); 178 179 return 1; 180} 181 182sub ArticleWriteAttachment { 183 my ( $Self, %Param ) = @_; 184 185 # check needed stuff 186 for my $Item (qw(Filename ContentType ArticleID UserID)) { 187 if ( !IsStringWithData( $Param{$Item} ) ) { 188 $Kernel::OM->Get('Kernel::System::Log')->Log( 189 Priority => 'error', 190 Message => "Need $Item!" 191 ); 192 return; 193 } 194 } 195 196 $Param{Filename} = $Kernel::OM->Get('Kernel::System::Main')->FilenameCleanUp( 197 Filename => $Param{Filename}, 198 Type => 'Local', 199 NoReplace => 1, 200 ); 201 202 my $NewFileName = $Param{Filename}; 203 my %UsedFile; 204 my %Index = $Self->ArticleAttachmentIndex( 205 ArticleID => $Param{ArticleID}, 206 ); 207 208 for my $IndexFile ( sort keys %Index ) { 209 $UsedFile{ $Index{$IndexFile}->{Filename} } = 1; 210 } 211 for ( my $i = 1; $i <= 50; $i++ ) { 212 if ( exists $UsedFile{$NewFileName} ) { 213 if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) { 214 $NewFileName = "$1-$i.$2"; 215 } 216 else { 217 $NewFileName = "$Param{Filename}-$i"; 218 } 219 } 220 } 221 222 # get file name 223 $Param{Filename} = $NewFileName; 224 225 # get attachment size 226 $Param{Filesize} = bytes::length( $Param{Content} ); 227 228 # get database object 229 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 230 231 # encode attachment if it's a postgresql backend!!! 232 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 233 234 $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Content} ); 235 236 $Param{Content} = encode_base64( $Param{Content} ); 237 } 238 239 # set content id in angle brackets 240 if ( $Param{ContentID} ) { 241 $Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/; 242 } 243 244 my $Disposition; 245 my $Filename; 246 if ( $Param{Disposition} ) { 247 ( $Disposition, $Filename ) = split ';', $Param{Disposition}; 248 } 249 $Disposition //= ''; 250 251 # write attachment to db 252 return if !$DBObject->Do( 253 SQL => ' 254 INSERT INTO article_data_mime_attachment (article_id, filename, content_type, content_size, 255 content, content_id, content_alternative, disposition, create_time, create_by, 256 change_time, change_by) 257 VALUES (?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)', 258 Bind => [ 259 \$Param{ArticleID}, \$Param{Filename}, \$Param{ContentType}, \$Param{Filesize}, 260 \$Param{Content}, \$Param{ContentID}, \$Param{ContentAlternative}, 261 \$Disposition, \$Param{UserID}, \$Param{UserID}, 262 ], 263 ); 264 return 1; 265} 266 267sub ArticlePlain { 268 my ( $Self, %Param ) = @_; 269 270 # check needed stuff 271 if ( !$Param{ArticleID} ) { 272 $Kernel::OM->Get('Kernel::System::Log')->Log( 273 Priority => 'error', 274 Message => "Need ArticleID!" 275 ); 276 return; 277 } 278 279 # prepare/filter ArticleID 280 $Param{ArticleID} = quotemeta( $Param{ArticleID} ); 281 $Param{ArticleID} =~ s/\0//g; 282 283 # get database object 284 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 285 286 # can't open article, try database 287 return if !$DBObject->Prepare( 288 SQL => 'SELECT body FROM article_data_mime_plain WHERE article_id = ?', 289 Bind => [ \$Param{ArticleID} ], 290 Encode => [0], 291 ); 292 293 my $Data; 294 while ( my @Row = $DBObject->FetchrowArray() ) { 295 296 # decode attachment if it's e. g. a postgresql backend!!! 297 if ( !$DBObject->GetDatabaseFunction('DirectBlob') && $Row[0] !~ m/ / ) { 298 $Data = decode_base64( $Row[0] ); 299 } 300 else { 301 $Data = $Row[0]; 302 } 303 } 304 return $Data if defined $Data; 305 306 # return if we only need to check one backend 307 return if !$Self->{CheckAllBackends}; 308 309 # return of only delete in my backend 310 return if $Param{OnlyMyBackend}; 311 312 return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticlePlain( 313 %Param, 314 OnlyMyBackend => 1, 315 ); 316} 317 318sub ArticleAttachmentIndexRaw { 319 my ( $Self, %Param ) = @_; 320 321 # check needed stuff 322 if ( !$Param{ArticleID} ) { 323 $Kernel::OM->Get('Kernel::System::Log')->Log( 324 Priority => 'error', 325 Message => 'Need ArticleID!' 326 ); 327 return; 328 } 329 330 my %Index; 331 my $Counter = 0; 332 333 # get database object 334 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 335 336 # try database 337 return if !$DBObject->Prepare( 338 SQL => ' 339 SELECT filename, content_type, content_size, content_id, content_alternative, 340 disposition 341 FROM article_data_mime_attachment 342 WHERE article_id = ? 343 ORDER BY filename, id', 344 Bind => [ \$Param{ArticleID} ], 345 ); 346 347 while ( my @Row = $DBObject->FetchrowArray() ) { 348 349 my $Disposition = $Row[5]; 350 if ( !$Disposition ) { 351 352 # if no content disposition is set images with content id should be inline 353 if ( $Row[3] && $Row[1] =~ m{image}i ) { 354 $Disposition = 'inline'; 355 } 356 357 # converted article body should be inline 358 elsif ( $Row[0] =~ m{file-[12]} ) { 359 $Disposition = 'inline'; 360 } 361 362 # all others including attachments with content id that are not images 363 # should NOT be inline 364 else { 365 $Disposition = 'attachment'; 366 } 367 } 368 369 # add the info the the hash 370 $Counter++; 371 $Index{$Counter} = { 372 Filename => $Row[0], 373 FilesizeRaw => $Row[2] || 0, 374 ContentType => $Row[1], 375 ContentID => $Row[3] || '', 376 ContentAlternative => $Row[4] || '', 377 Disposition => $Disposition, 378 }; 379 } 380 381 # return existing index 382 return %Index if %Index; 383 384 # return if we only need to check one backend 385 return if !$Self->{CheckAllBackends}; 386 387 # return if only delete in my backend 388 return if $Param{OnlyMyBackend}; 389 390 return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS') 391 ->ArticleAttachmentIndexRaw( 392 %Param, 393 OnlyMyBackend => 1, 394 ); 395} 396 397sub ArticleAttachment { 398 my ( $Self, %Param ) = @_; 399 400 # check needed stuff 401 for my $Item (qw(ArticleID FileID)) { 402 if ( !$Param{$Item} ) { 403 $Kernel::OM->Get('Kernel::System::Log')->Log( 404 Priority => 'error', 405 Message => "Need $Item!" 406 ); 407 return; 408 } 409 } 410 411 # prepare/filter ArticleID 412 $Param{ArticleID} = quotemeta( $Param{ArticleID} ); 413 $Param{ArticleID} =~ s/\0//g; 414 415 # get attachment index 416 my %Index = $Self->ArticleAttachmentIndex( 417 ArticleID => $Param{ArticleID}, 418 ); 419 420 return if !$Index{ $Param{FileID} }; 421 my %Data = %{ $Index{ $Param{FileID} } }; 422 423 # get database object 424 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 425 426 # try database 427 return if !$DBObject->Prepare( 428 SQL => ' 429 SELECT id 430 FROM article_data_mime_attachment 431 WHERE article_id = ? 432 ORDER BY filename, id', 433 Bind => [ \$Param{ArticleID} ], 434 Limit => $Param{FileID}, 435 ); 436 437 my $AttachmentID; 438 while ( my @Row = $DBObject->FetchrowArray() ) { 439 $AttachmentID = $Row[0]; 440 } 441 442 return if !$DBObject->Prepare( 443 SQL => ' 444 SELECT content_type, content, content_id, content_alternative, disposition, filename 445 FROM article_data_mime_attachment 446 WHERE id = ?', 447 Bind => [ \$AttachmentID ], 448 Encode => [ 1, 0, 0, 0, 1, 1 ], 449 ); 450 451 while ( my @Row = $DBObject->FetchrowArray() ) { 452 453 $Data{ContentType} = $Row[0]; 454 455 # decode attachment if it's e. g. a postgresql backend!!! 456 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 457 $Data{Content} = decode_base64( $Row[1] ); 458 } 459 else { 460 $Data{Content} = $Row[1]; 461 } 462 $Data{ContentID} = $Row[2] || ''; 463 $Data{ContentAlternative} = $Row[3] || ''; 464 $Data{Disposition} = $Row[4]; 465 $Data{Filename} = $Row[5]; 466 } 467 468 if ( !$Data{Disposition} ) { 469 470 # if no content disposition is set images with content id should be inline 471 if ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) { 472 $Data{Disposition} = 'inline'; 473 } 474 475 # converted article body should be inline 476 elsif ( $Data{Filename} =~ m{file-[12]} ) { 477 $Data{Disposition} = 'inline'; 478 } 479 480 # all others including attachments with content id that are not images 481 # should NOT be inline 482 else { 483 $Data{Disposition} = 'attachment'; 484 } 485 } 486 487 return %Data if defined $Data{Content}; 488 489 # return if we only need to check one backend 490 return if !$Self->{CheckAllBackends}; 491 492 # return if only delete in my backend 493 return if $Param{OnlyMyBackend}; 494 495 return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleAttachment( 496 %Param, 497 OnlyMyBackend => 1, 498 ); 499} 500 5011; 502 503=head1 TERMS AND CONDITIONS 504 505This software is part of the OTRS project (L<https://otrs.org/>). 506 507This software comes with ABSOLUTELY NO WARRANTY. For details, see 508the enclosed file COPYING for license information (GPL). If you 509did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 510 511=cut 512