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::Web::UploadCache::FS;
10
11use strict;
12use warnings;
13
14our @ObjectDependencies = (
15    'Kernel::Config',
16    'Kernel::System::Log',
17    'Kernel::System::Main',
18);
19
20sub new {
21    my ( $Type, %Param ) = @_;
22
23    # allocate new hash for object
24    my $Self = {};
25    bless( $Self, $Type );
26
27    $Self->{TempDir} = $Kernel::OM->Get('Kernel::Config')->Get('TempDir') . '/upload_cache';
28
29    if ( !-d $Self->{TempDir} ) {
30        mkdir $Self->{TempDir};
31    }
32
33    return $Self;
34}
35
36sub FormIDCreate {
37    my ( $Self, %Param ) = @_;
38
39    # return requested form id
40    return time() . '.' . rand(12341241);
41}
42
43sub FormIDRemove {
44    my ( $Self, %Param ) = @_;
45
46    if ( !$Param{FormID} ) {
47        $Kernel::OM->Get('Kernel::System::Log')->Log(
48            Priority => 'error',
49            Message  => 'Need FormID!'
50        );
51        return;
52    }
53
54    return if !$Self->_FormIDValidate( $Param{FormID} );
55
56    my $Directory = $Self->{TempDir} . '/' . $Param{FormID};
57
58    if ( !-d $Directory ) {
59        return 1;
60    }
61
62    # get main object
63    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
64
65    my @List = $MainObject->DirectoryRead(
66        Directory => $Directory,
67        Filter    => "*",
68    );
69
70    my @Data;
71    for my $File (@List) {
72        $MainObject->FileDelete(
73            Location => $File,
74        );
75    }
76
77    if ( !rmdir($Directory) ) {
78        $Kernel::OM->Get('Kernel::System::Log')->Log(
79            Priority => 'error',
80            Message  => "Can't remove: $Directory: $!!",
81        );
82    }
83
84    return 1;
85}
86
87sub FormIDAddFile {
88    my ( $Self, %Param ) = @_;
89
90    for (qw(FormID Filename ContentType)) {
91        if ( !$Param{$_} ) {
92            $Kernel::OM->Get('Kernel::System::Log')->Log(
93                Priority => 'error',
94                Message  => "Need $_!"
95            );
96            return;
97        }
98    }
99
100    return if !$Self->_FormIDValidate( $Param{FormID} );
101
102    $Param{Content} = '' if !defined( $Param{Content} );
103
104    # create content id
105    my $ContentID   = $Param{ContentID};
106    my $Disposition = $Param{Disposition} || '';
107    if ( !$ContentID && lc $Disposition eq 'inline' ) {
108
109        my $Random = rand 999999;
110        my $FQDN   = $Kernel::OM->Get('Kernel::Config')->Get('FQDN');
111
112        $ContentID = "$Disposition$Random.$Param{FormID}\@$FQDN";
113    }
114
115    # create cache subdirectory if not exist
116    my $Directory = $Self->{TempDir} . '/' . $Param{FormID};
117    if ( !-d $Directory ) {
118
119        # Create directory. This could fail if another process creates the
120        #   same directory, so don't use the return value.
121        File::Path::mkpath( $Directory, 0, 0770 );    ## no critic
122
123        if ( !-d $Directory ) {
124            $Kernel::OM->Get('Kernel::System::Log')->Log(
125                Priority => 'error',
126                Message  => "Can't create directory '$Directory': $!",
127            );
128            return;
129        }
130    }
131
132    # get main object
133    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
134
135    # files must readable for creator
136    return if !$MainObject->FileWrite(
137        Directory  => $Directory,
138        Filename   => "$Param{Filename}",
139        Content    => \$Param{Content},
140        Mode       => 'binmode',
141        Permission => '640',
142        NoReplace  => 1,
143    );
144    return if !$MainObject->FileWrite(
145        Directory  => $Directory,
146        Filename   => "$Param{Filename}.ContentType",
147        Content    => \$Param{ContentType},
148        Mode       => 'binmode',
149        Permission => '640',
150        NoReplace  => 1,
151    );
152    return if !$MainObject->FileWrite(
153        Directory  => $Directory,
154        Filename   => "$Param{Filename}.ContentID",
155        Content    => \$ContentID,
156        Mode       => 'binmode',
157        Permission => '640',
158        NoReplace  => 1,
159    );
160    return if !$MainObject->FileWrite(
161        Directory  => $Directory,
162        Filename   => "$Param{Filename}.Disposition",
163        Content    => \$Disposition,
164        Mode       => 'binmode',
165        Permission => '644',
166        NoReplace  => 1,
167    );
168    return 1;
169}
170
171sub FormIDRemoveFile {
172    my ( $Self, %Param ) = @_;
173
174    for (qw(FormID FileID)) {
175        if ( !$Param{$_} ) {
176            $Kernel::OM->Get('Kernel::System::Log')->Log(
177                Priority => 'error',
178                Message  => "Need $_!"
179            );
180            return;
181        }
182    }
183
184    return if !$Self->_FormIDValidate( $Param{FormID} );
185
186    my @Index = @{ $Self->FormIDGetAllFilesMeta(%Param) };
187
188    # finish if files have been already removed by other process
189    return if !@Index;
190
191    my $ID   = $Param{FileID} - 1;
192    my %File = %{ $Index[$ID] };
193
194    my $Directory = $Self->{TempDir} . '/' . $Param{FormID};
195
196    if ( !-d $Directory ) {
197        return 1;
198    }
199
200    # get main object
201    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
202
203    $MainObject->FileDelete(
204        Directory => $Directory,
205        Filename  => "$File{Filename}",
206        NoReplace => 1,
207    );
208    $MainObject->FileDelete(
209        Directory => $Directory,
210        Filename  => "$File{Filename}.ContentType",
211        NoReplace => 1,
212    );
213    $MainObject->FileDelete(
214        Directory => $Directory,
215        Filename  => "$File{Filename}.ContentID",
216        NoReplace => 1,
217    );
218    $MainObject->FileDelete(
219        Directory => $Directory,
220        Filename  => "$File{Filename}.Disposition",
221        NoReplace => 1,
222    );
223
224    return 1;
225}
226
227sub FormIDGetAllFilesData {
228    my ( $Self, %Param ) = @_;
229
230    if ( !$Param{FormID} ) {
231        $Kernel::OM->Get('Kernel::System::Log')->Log(
232            Priority => 'error',
233            Message  => 'Need FormID!'
234        );
235        return;
236    }
237
238    my @Data;
239
240    return \@Data if !$Self->_FormIDValidate( $Param{FormID} );
241
242    my $Directory = $Self->{TempDir} . '/' . $Param{FormID};
243
244    if ( !-d $Directory ) {
245        return \@Data;
246    }
247
248    # get main object
249    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
250
251    my @List = $MainObject->DirectoryRead(
252        Directory => $Directory,
253        Filter    => "*",
254    );
255
256    my $Counter = 0;
257
258    FILE:
259    for my $File (@List) {
260
261        # ignore meta files
262        next FILE if $File =~ /\.ContentType$/;
263        next FILE if $File =~ /\.ContentID$/;
264        next FILE if $File =~ /\.Disposition$/;
265
266        $Counter++;
267        my $FileSize = -s $File;
268
269        # human readable file size
270        if ( defined $FileSize ) {
271
272            # remove meta data in files
273            if ( $FileSize > 30 ) {
274                $FileSize = $FileSize - 30;
275            }
276        }
277        my $Content = $MainObject->FileRead(
278            Location => $File,
279            Mode     => 'binmode',    # optional - binmode|utf8
280        );
281        next FILE if !$Content;
282
283        my $ContentType = $MainObject->FileRead(
284            Location => "$File.ContentType",
285            Mode     => 'binmode',             # optional - binmode|utf8
286        );
287        next FILE if !$ContentType;
288
289        my $ContentID = $MainObject->FileRead(
290            Location => "$File.ContentID",
291            Mode     => 'binmode',             # optional - binmode|utf8
292        );
293        next FILE if !$ContentID;
294
295        # verify if content id is empty, set to undef
296        if ( !${$ContentID} ) {
297            ${$ContentID} = undef;
298        }
299
300        my $Disposition = $MainObject->FileRead(
301            Location => "$File.Disposition",
302            Mode     => 'binmode',             # optional - binmode|utf8
303        );
304        next FILE if !$Disposition;
305
306        # strip filename
307        $File =~ s/^.*\/(.+?)$/$1/;
308        push(
309            @Data,
310            {
311                Content     => ${$Content},
312                ContentID   => ${$ContentID},
313                ContentType => ${$ContentType},
314                Filename    => $File,
315                Filesize    => $FileSize,
316                FileID      => $Counter,
317                Disposition => ${$Disposition},
318            },
319        );
320    }
321    return \@Data;
322
323}
324
325sub FormIDGetAllFilesMeta {
326    my ( $Self, %Param ) = @_;
327
328    if ( !$Param{FormID} ) {
329        $Kernel::OM->Get('Kernel::System::Log')->Log(
330            Priority => 'error',
331            Message  => 'Need FormID!'
332        );
333        return;
334    }
335
336    my @Data;
337
338    return \@Data if !$Self->_FormIDValidate( $Param{FormID} );
339
340    my $Directory = $Self->{TempDir} . '/' . $Param{FormID};
341
342    if ( !-d $Directory ) {
343        return \@Data;
344    }
345
346    # get main object
347    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
348
349    my @List = $MainObject->DirectoryRead(
350        Directory => $Directory,
351        Filter    => "*",
352    );
353
354    my $Counter = 0;
355
356    FILE:
357    for my $File (@List) {
358
359        # ignore meta files
360        next FILE if $File =~ /\.ContentType$/;
361        next FILE if $File =~ /\.ContentID$/;
362        next FILE if $File =~ /\.Disposition$/;
363
364        $Counter++;
365        my $FileSize = -s $File;
366
367        # human readable file size
368        if ( defined $FileSize ) {
369
370            # remove meta data in files
371            if ( $FileSize > 30 ) {
372                $FileSize = $FileSize - 30;
373            }
374        }
375
376        my $ContentType = $MainObject->FileRead(
377            Location => "$File.ContentType",
378            Mode     => 'binmode',             # optional - binmode|utf8
379        );
380        next FILE if !$ContentType;
381
382        my $ContentID = $MainObject->FileRead(
383            Location => "$File.ContentID",
384            Mode     => 'binmode',             # optional - binmode|utf8
385        );
386        next FILE if !$ContentID;
387
388        # verify if content id is empty, set to undef
389        if ( !${$ContentID} ) {
390            ${$ContentID} = undef;
391        }
392
393        my $Disposition = $MainObject->FileRead(
394            Location => "$File.Disposition",
395            Mode     => 'binmode',             # optional - binmode|utf8
396        );
397        next FILE if !$Disposition;
398
399        # strip filename
400        $File =~ s/^.*\/(.+?)$/$1/;
401        push(
402            @Data,
403            {
404                ContentID   => ${$ContentID},
405                ContentType => ${$ContentType},
406                Filename    => $File,
407                Filesize    => $FileSize,
408                FileID      => $Counter,
409                Disposition => ${$Disposition},
410            },
411        );
412    }
413    return \@Data;
414}
415
416sub FormIDCleanUp {
417    my ( $Self, %Param ) = @_;
418
419    # get main object
420    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
421
422    my $RetentionTime = int( time() - 86400 );        # remove subdirs older than 24h
423    my @List          = $MainObject->DirectoryRead(
424        Directory => $Self->{TempDir},
425        Filter    => '*'
426    );
427
428    SUBDIR:
429    for my $Subdir (@List) {
430        my $SubdirTime = $Subdir;
431
432        if ( $SubdirTime =~ /^.*\/\d+\..+$/ ) {
433            $SubdirTime =~ s/^.*\/(\d+?)\..+$/$1/;
434        }
435        else {
436            $Kernel::OM->Get('Kernel::System::Log')->Log(
437                Priority => 'error',
438                Message =>
439                    "Won't delete upload cache directory $Subdir: timestamp in directory name not found! Please fix it manually.",
440            );
441            next SUBDIR;
442        }
443
444        if ( $RetentionTime > $SubdirTime ) {
445            my @Sublist = $MainObject->DirectoryRead(
446                Directory => $Subdir,
447                Filter    => "*",
448            );
449
450            for my $File (@Sublist) {
451                $MainObject->FileDelete(
452                    Location => $File,
453                );
454            }
455
456            if ( !rmdir($Subdir) ) {
457                $Kernel::OM->Get('Kernel::System::Log')->Log(
458                    Priority => 'error',
459                    Message  => "Can't remove: $Subdir: $!!",
460                );
461                next SUBDIR;
462            }
463        }
464    }
465
466    return 1;
467}
468
469sub _FormIDValidate {
470    my ( $Self, $FormID ) = @_;
471
472    return if !$FormID;
473
474    if ( $FormID !~ m{^ \d+ \. \d+ \. \d+ $}xms ) {
475        $Kernel::OM->Get('Kernel::System::Log')->Log(
476            Priority => 'error',
477            Message  => 'Invalid FormID!',
478        );
479        return;
480    }
481
482    return 1;
483}
484
4851;
486