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