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::FormDraft;
10
11use strict;
12use warnings;
13
14use Kernel::System::VariableCheck qw(:all);
15use MIME::Base64;
16use Storable;
17
18our @ObjectDependencies = (
19    'Kernel::System::Cache',
20    'Kernel::System::DB',
21    'Kernel::System::Log',
22    'Kernel::System::Storable',
23);
24
25=head1 NAME
26
27Kernel::System::FormDraft - draft lib
28
29=head1 SYNOPSIS
30
31All draft functions.
32
33=head1 PUBLIC INTERFACE
34
35=over 4
36
37=cut
38
39=item new()
40
41create an object
42
43    use Kernel::System::ObjectManager;
44    local $Kernel::OM = Kernel::System::ObjectManager->new();
45    my $FormDraftObject = $Kernel::OM->Get('Kernel::System::FormDraft');
46
47=cut
48
49sub new {
50    my ( $Type, %Param ) = @_;
51
52    # allocate new hash for object
53    my $Self = {};
54    bless( $Self, $Type );
55
56    $Self->{CacheType} = 'FormDraft';
57    $Self->{CacheTTL}  = 60 * 60 * 24 * 30;
58
59    return $Self;
60}
61
62=item FormDraftGet()
63
64get draft attributes
65
66    my $FormDraft = $FormDraftObject->FormDraftGet(
67        FormDraftID    => 123,
68        GetContent => 1,                # optional, default 1
69        UserID     => 123,
70    );
71
72Returns (with GetContent = 0):
73
74    $FormDraft = {
75        FormDraftID    => 123,
76        ObjectType => 'Ticket',
77        ObjectID   => 12,
78        Action     => 'AgentTicketCompose',
79        CreateTime => '2016-04-07 15:41:15',
80        CreateBy   => 1,
81        ChangeTime => '2016-04-07 15:59:45',
82        ChangeBy   => 2,
83    };
84
85Returns (without GetContent or GetContent = 1):
86
87    $FormDraft = {
88        FormData => {
89            InformUserID => [ 123, 124, ],
90            Subject      => 'Request for information',
91            ...
92        },
93        FileData => [
94            {
95                'Content'     => 'Dear customer\n\nthank you!',
96                'ContentType' => 'text/plain',
97                'ContentID'   => undef,                                 # optional
98                'Filename'    => 'thankyou.txt',
99                'Filesize'    => 25,
100                'FileID'      => 1,
101                'Disposition' => 'attachment',
102            },
103            ...
104        ],
105        FormDraftID    => 123,
106        ObjectType => 'Ticket',
107        ObjectID   => 12,
108        Action     => 'AgentTicketCompose',
109        CreateTime => '2016-04-07 15:41:15',
110        CreateBy   => 1,
111        ChangeTime => '2016-04-07 15:59:45',
112        ChangeBy   => 2,
113        Title      => 'my draft',
114    };
115
116=cut
117
118sub FormDraftGet {
119    my ( $Self, %Param ) = @_;
120
121    # check needed stuff
122    for my $Needed (qw(FormDraftID UserID)) {
123        if ( !$Param{$Needed} ) {
124            $Kernel::OM->Get('Kernel::System::Log')->Log(
125                Priority => 'error',
126                Message  => "Need $Needed!",
127            );
128            return;
129        }
130    }
131
132    # determine if we should get content
133    $Param{GetContent} //= 1;
134    if ( $Param{GetContent} !~ m{ \A [01] \z }xms ) {
135        $Kernel::OM->Get('Kernel::System::Log')->Log(
136            Priority => 'error',
137            Message  => "Invalid value '$Param{GetContent}' for GetContent!",
138        );
139        return;
140    }
141
142    # check cache
143    my $CacheKey = 'FormDraftGet::GetContent' . $Param{GetContent} . '::ID' . $Param{FormDraftID};
144    my $Cache    = $Kernel::OM->Get('Kernel::System::Cache')->Get(
145        Type => $Self->{CacheType},
146        Key  => $CacheKey,
147    );
148    return $Cache if $Cache;
149
150    # get database object
151    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
152
153    # prepare query
154    my $SQL =
155        'SELECT id, object_type, object_id, action, title,'
156        . ' create_time, create_by, change_time, change_by';
157
158    my @EncodeColumns = ( 1, 1, 1, 1, 1, 1, 1, 1, 1 );
159    if ( $Param{GetContent} ) {
160        $SQL .= ', content';
161        push @EncodeColumns, 0;
162    }
163    $SQL .= ' FROM form_draft WHERE id = ?';
164
165    # ask the database
166    return if !$DBObject->Prepare(
167        SQL    => $SQL,
168        Bind   => [ \$Param{FormDraftID} ],
169        Limit  => 1,
170        Encode => \@EncodeColumns,
171    );
172
173    # fetch the result
174    my %FormDraft;
175    while ( my @Row = $DBObject->FetchrowArray() ) {
176        %FormDraft = (
177            FormDraftID => $Row[0],
178            ObjectType  => $Row[1],
179            ObjectID    => $Row[2],
180            Action      => $Row[3],
181            Title       => $Row[4] || '',
182            CreateTime  => $Row[5],
183            CreateBy    => $Row[6],
184            ChangeTime  => $Row[7],
185            ChangeBy    => $Row[8],
186        );
187
188        if ( $Param{GetContent} ) {
189
190            my $RawContent      = $Row[9] // {};
191            my $StorableContent = $RawContent;
192
193            if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
194                $StorableContent = MIME::Base64::decode_base64($RawContent);
195            }
196
197            # convert form and file data from yaml
198            my $Content = $Kernel::OM->Get('Kernel::System::Storable')->Deserialize( Data => $StorableContent ) // {};
199
200            $FormDraft{FormData} = $Content->{FormData};
201            $FormDraft{FileData} = $Content->{FileData};
202        }
203    }
204
205    # no data found
206    if ( !%FormDraft ) {
207        $Kernel::OM->Get('Kernel::System::Log')->Log(
208            Priority => 'error',
209            Message  => "FormDraft with ID '$Param{FormDraftID}' not found!",
210        );
211        return;
212    }
213
214    # always cache version without content
215    my $CacheKeyNoContent;
216    my %FormDraftNoContent;
217    if ( $Param{GetContent} ) {
218        $CacheKeyNoContent  = 'FormDraftGet::GetContent0::ID' . $Param{FormDraftID};
219        %FormDraftNoContent = %{ Storable::dclone( \%FormDraft ) };
220        delete $FormDraftNoContent{FileData};
221        delete $FormDraftNoContent{FormData};
222    }
223    else {
224        $CacheKeyNoContent  = $CacheKey;
225        %FormDraftNoContent = %FormDraft;
226    }
227    $Kernel::OM->Get('Kernel::System::Cache')->Set(
228        Type  => $Self->{CacheType},
229        Key   => $CacheKeyNoContent,
230        Value => \%FormDraftNoContent,
231    );
232
233    return \%FormDraft if !$Param{GetContent};
234
235    # set cache with content (shorter cache time due to potentially large content)
236    $Kernel::OM->Get('Kernel::System::Cache')->Set(
237        Type  => $Self->{CacheType},
238        TTL   => 60 * 60,
239        Key   => $CacheKey,
240        Value => \%FormDraft,
241    );
242
243    return \%FormDraft;
244}
245
246=item FormDraftAdd()
247
248add a new draft
249
250    my $Success = $FormDraftObject->FormDraftAdd(
251        FormData => {
252            InformUserID => [ 123, 124, ],
253            Subject      => 'Request for information',
254            ...
255        },
256        FileData => [                                           # optional
257            {
258                'Content'     => 'Dear customer\n\nthank you!',
259                'ContentType' => 'text/plain',
260                'ContentID'   => undef,                         # optional
261                'Filename'    => 'thankyou.txt',
262                'Filesize'    => 25,
263                'FileID'      => 1,
264                'Disposition' => 'attachment',
265            },
266            ...
267        ],
268        ObjectType => 'Ticket',
269        ObjectID   => 12,
270        Action     => 'AgentTicketCompose',
271        Title      => 'my draft',                               # optional
272        UserID     => 123,
273    );
274
275=cut
276
277sub FormDraftAdd {
278    my ( $Self, %Param ) = @_;
279
280    # check needed stuff
281    for my $Needed (qw(FormData ObjectType Action)) {
282        if ( !$Param{$Needed} ) {
283            $Kernel::OM->Get('Kernel::System::Log')->Log(
284                Priority => 'error',
285                Message  => "Need $Needed!",
286            );
287            return;
288        }
289    }
290    for my $Needed (qw(ObjectID UserID)) {
291        if ( !IsInteger( $Param{$Needed} ) ) {
292            $Kernel::OM->Get('Kernel::System::Log')->Log(
293                Priority => 'error',
294                Message  => "Need $Needed!",
295            );
296            return;
297        }
298    }
299
300    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
301
302    # serialize form and file data
303    my $StorableContent = $Kernel::OM->Get('Kernel::System::Storable')->Serialize(
304        Data => {
305            FormData => $Param{FormData},
306            FileData => $Param{FileData} || [],
307        },
308    );
309
310    my $Content = $StorableContent;
311    if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
312        $Content = MIME::Base64::encode_base64($StorableContent);
313    }
314
315    # add to database
316    return if !$DBObject->Do(
317        SQL =>
318            'INSERT INTO form_draft'
319            . ' (object_type, object_id, action, title, content, create_time, create_by, change_time, change_by)'
320            . ' VALUES (?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
321        Bind => [
322            \$Param{ObjectType}, \$Param{ObjectID}, \$Param{Action}, \$Param{Title}, \$Content,
323            \$Param{UserID}, \$Param{UserID},
324        ],
325    );
326
327    # delete affected caches
328    $Self->_DeleteAffectedCaches(%Param);
329
330    return 1;
331}
332
333=item FormDraftUpdate()
334
335update an existing draft
336
337    my $Success = $FormDraftObject->FormDraftUpdate(
338        FormData => {
339            InformUserID => [ 123, 124, ],
340            Subject      => 'Request for information',
341            ...
342        },
343        FileData => [                                           # optional
344            {
345                'Content'     => 'Dear customer\n\nthank you!',
346                'ContentType' => 'text/plain',
347                'ContentID'   => undef,                         # optional
348                'Filename'    => 'thankyou.txt',
349                'Filesize'    => 25,
350                'FileID'      => 1,
351                'Disposition' => 'attachment',
352            },
353            ...
354        ],
355        ObjectType  => 'Ticket',
356        ObjectID    => 12,
357        Action      => 'AgentTicketCompose',
358        Title       => 'my draft',
359        FormDraftID => 1,
360        UserID      => 123,
361    );
362
363=cut
364
365sub FormDraftUpdate {
366    my ( $Self, %Param ) = @_;
367
368    # check needed stuff
369    for my $Needed (qw(FormData ObjectType Action)) {
370        if ( !$Param{$Needed} ) {
371            $Kernel::OM->Get('Kernel::System::Log')->Log(
372                Priority => 'error',
373                Message  => "Need $Needed!",
374            );
375            return;
376        }
377    }
378    for my $Needed (qw(ObjectID FormDraftID UserID)) {
379        if ( !IsInteger( $Param{$Needed} ) ) {
380            $Kernel::OM->Get('Kernel::System::Log')->Log(
381                Priority => 'error',
382                Message  => "Need $Needed!",
383            );
384            return;
385        }
386    }
387
388    # check if specified draft already exists and do sanity checks
389    my $FormDraft = $Self->FormDraftGet(
390        FormDraftID => $Param{FormDraftID},
391        GetContent  => 0,
392        UserID      => $Param{UserID},
393    );
394    if ( !$FormDraft ) {
395        $Kernel::OM->Get('Kernel::System::Log')->Log(
396            Priority => 'error',
397            Message  => "FormDraft with ID '$Param{FormDraftID}' not found!",
398        );
399        return;
400    }
401    VALIDATEPARAM:
402    for my $ValidateParam (qw(ObjectType ObjectID Action)) {
403        next VALIDATEPARAM if $Param{$ValidateParam} eq $FormDraft->{$ValidateParam};
404
405        $Kernel::OM->Get('Kernel::System::Log')->Log(
406            Priority => 'error',
407            Message =>
408                "Param '$ValidateParam' for draft with ID '$Param{FormDraftID}'"
409                . " must not be changed on update!",
410        );
411        return;
412    }
413
414    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
415
416    # serialize form and file data
417    my $StorableContent = $Kernel::OM->Get('Kernel::System::Storable')->Serialize(
418        Data => {
419            FormData => $Param{FormData},
420            FileData => $Param{FileData} || [],
421        },
422    );
423
424    my $Content = $StorableContent;
425    if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
426        $Content = MIME::Base64::encode_base64($StorableContent);
427    }
428
429    # add to database
430    return if !$DBObject->Do(
431        SQL =>
432            'UPDATE form_draft'
433            . ' SET title = ?, content = ?, change_time = current_timestamp, change_by = ?'
434            . ' WHERE id = ?',
435        Bind => [ \$Param{Title}, \$Content, \$Param{UserID}, \$Param{FormDraftID}, ],
436    );
437
438    # delete affected caches
439    $Self->_DeleteAffectedCaches(%Param);
440
441    return 1;
442}
443
444=item FormDraftDelete()
445
446remove draft
447
448    my $Success = $FormDraftObject->FormDraftDelete(
449        FormDraftID => 123,
450        UserID  => 123,
451    );
452
453=cut
454
455sub FormDraftDelete {
456    my ( $Self, %Param ) = @_;
457
458    # check needed stuff
459    for my $Needed (qw(FormDraftID UserID)) {
460        if ( !$Param{$Needed} ) {
461            $Kernel::OM->Get('Kernel::System::Log')->Log(
462                Priority => 'error',
463                Message  => "Need $Needed!",
464            );
465            return;
466        }
467    }
468
469    # get draft data as sanity check and to determine which caches should be removed
470    # use database query directly (we don't need raw content)
471    my $FormDraft = $Self->FormDraftGet(
472        FormDraftID => $Param{FormDraftID},
473        GetContent  => 0,
474        UserID      => $Param{UserID},
475    );
476    if ( !$FormDraft ) {
477        $Kernel::OM->Get('Kernel::System::Log')->Log(
478            Priority => 'error',
479            Message  => "FormDraft with ID '$Param{FormDraftID}' not found!",
480        );
481        return;
482    }
483
484    # remove from database
485    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
486        SQL  => 'DELETE FROM form_draft WHERE id = ?',
487        Bind => [ \$Param{FormDraftID} ],
488    );
489
490    # delete affected caches
491    $Self->_DeleteAffectedCaches( %{$FormDraft} );
492
493    return 1;
494}
495
496=item FormDraftListGet()
497
498get list of drafts, optionally filtered by object type, object id and action
499
500    my $FormDraftList = $FormDraftObject->FormDraftListGet(
501        ObjectType => 'Ticket',             # optional
502        ObjectID   => 123,                  # optional
503        Action     => 'AgentTicketCompose', # optional
504        UserID     => 123,
505    );
506
507Returns:
508
509    $FormDraftList = [
510        {
511            FormDraftID => 123,
512            ObjectType  => 'Ticket',
513            ObjectID    => 12,
514            Action      => 'AgentTicketCompose',
515            Title       => 'my draft',
516            CreateTime  => '2016-04-07 15:41:15',
517            CreateBy    => 1,
518            ChangeTime  => '2016-04-07 15:59:45',
519            ChangeBy    => 2,
520        },
521        ...
522    ];
523
524=cut
525
526sub FormDraftListGet {
527    my ( $Self, %Param ) = @_;
528
529    # check needed stuff
530    if ( !$Param{UserID} ) {
531        $Kernel::OM->Get('Kernel::System::Log')->Log(
532            Priority => 'error',
533            Message  => 'Need UserID!',
534        );
535        return;
536    }
537
538    # check cache
539    my $CacheKey = 'FormDraftListGet';
540    RESTRICTION:
541    for my $Restriction (qw(ObjectType Action ObjectID)) {
542        next RESTRICTION if !IsStringWithData( $Param{$Restriction} );
543        $CacheKey .= '::' . $Restriction . $Param{$Restriction};
544    }
545    my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
546        Type => $Self->{CacheType},
547        Key  => $CacheKey,
548    );
549    return $Cache if $Cache;
550
551    # prepare database restrictions by given parameters
552    my %ParamToField = (
553        ObjectType => 'object_type',
554        Action     => 'action',
555        ObjectID   => 'object_id',
556    );
557    my $SQLExt = '';
558    my @Bind;
559    RESTRICTION:
560    for my $Restriction (qw(ObjectType Action ObjectID)) {
561        next RESTRICTION if !IsStringWithData( $Param{$Restriction} );
562        $SQLExt .= $SQLExt ? ' AND ' : ' WHERE ';
563        $SQLExt .= $ParamToField{$Restriction} . ' = ?';
564        push @Bind, \$Param{$Restriction};
565    }
566
567    # get database object
568    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
569
570    # ask the database
571    return if !$DBObject->Prepare(
572        SQL =>
573            'SELECT id, object_type, object_id, action, title,'
574            . ' create_time, create_by, change_time, change_by'
575            . ' FROM form_draft' . $SQLExt
576            . ' ORDER BY id ASC',
577        Bind => \@Bind,
578    );
579
580    # fetch the results
581    my @FormDrafts;
582    while ( my @Row = $DBObject->FetchrowArray() ) {
583        push @FormDrafts, {
584            FormDraftID => $Row[0],
585            ObjectType  => $Row[1],
586            ObjectID    => $Row[2],
587            Action      => $Row[3],
588            Title       => $Row[4] || '',
589            CreateTime  => $Row[5],
590            CreateBy    => $Row[6],
591            ChangeTime  => $Row[7],
592            ChangeBy    => $Row[8],
593        };
594    }
595
596    # set cache
597    $Kernel::OM->Get('Kernel::System::Cache')->Set(
598        Type  => $Self->{CacheType},
599        Key   => $CacheKey,
600        Value => \@FormDrafts,
601    );
602
603    return \@FormDrafts;
604}
605
606=item _DeleteAffectedCaches()
607
608remove all potentially affected caches
609
610    my $Success = $FormDraftObject->_DeleteAffectedCaches(
611        FormDraftID    => 1,                               # optional
612        ObjectType => 'Ticket',
613        ObjectID   => 12,
614        Action     => 'AgentTicketCompose',
615    );
616
617=cut
618
619sub _DeleteAffectedCaches {
620    my ( $Self, %Param ) = @_;
621
622    # prepare affected cache keys
623    my @CacheKeys = (
624        'FormDraftListGet',
625        'FormDraftListGet::ObjectType' . $Param{ObjectType},
626        'FormDraftListGet::Action' . $Param{Action},
627        'FormDraftListGet::ObjectID' . $Param{ObjectID},
628        'FormDraftListGet::ObjectType' . $Param{ObjectType}
629            . '::Action' . $Param{Action},
630        'FormDraftListGet::ObjectType' . $Param{ObjectType}
631            . '::ObjectID' . $Param{ObjectID},
632        'FormDraftListGet::Action' . $Param{Action}
633            . '::ObjectID' . $Param{ObjectID},
634        'FormDraftListGet::ObjectType' . $Param{ObjectType}
635            . '::Action' . $Param{Action}
636            . '::ObjectID' . $Param{ObjectID},
637    );
638    if ( $Param{FormDraftID} ) {
639        push @CacheKeys,
640            'FormDraftGet::GetContent0::ID' . $Param{FormDraftID},
641            'FormDraftGet::GetContent1::ID' . $Param{FormDraftID};
642    }
643
644    # delete affected caches
645    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
646    for my $CacheKey (@CacheKeys) {
647        $CacheObject->Delete(
648            Type => $Self->{CacheType},
649            Key  => $CacheKey,
650        );
651    }
652
653    return 1;
654}
655
6561;
657
658=back
659
660=head1 TERMS AND CONDITIONS
661
662This software is part of the OTRS project (L<https://otrs.org/>).
663
664This software comes with ABSOLUTELY NO WARRANTY. For details, see
665the enclosed file COPYING for license information (GPL). If you
666did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
667
668=cut
669