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