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::Calendar;
10
11use strict;
12use warnings;
13
14use Digest::MD5;
15use MIME::Base64 ();
16
17use Kernel::System::EventHandler;
18use Kernel::System::VariableCheck qw(:all);
19use vars qw(@ISA);
20
21our @ObjectDependencies = (
22    'Kernel::Config',
23    'Kernel::System::Cache',
24    'Kernel::System::Calendar::Appointment',
25    'Kernel::System::DynamicField',
26    'Kernel::System::Encode',
27    'Kernel::System::Group',
28    'Kernel::System::DB',
29    'Kernel::System::Log',
30    'Kernel::System::Main',
31    'Kernel::System::Queue',
32    'Kernel::System::Storable',
33    'Kernel::System::Ticket',
34    'Kernel::System::Valid',
35);
36
37=head1 NAME
38
39Kernel::System::Calendar - calendar lib
40
41=head1 DESCRIPTION
42
43All calendar functions.
44
45=head1 PUBLIC INTERFACE
46
47=head2 new()
48
49create an object. Do not use it directly, instead use:
50
51    use Kernel::System::ObjectManager;
52    local $Kernel::OM = Kernel::System::ObjectManager->new();
53    my $CalendarObject = $Kernel::OM->Get('Kernel::System::Calendar');
54
55=cut
56
57sub new {
58    my ( $Type, %Param ) = @_;
59
60    # allocate new hash for object
61    my $Self = {%Param};
62    bless( $Self, $Type );
63
64    @ISA = qw(
65        Kernel::System::EventHandler
66    );
67
68    # init of event handler
69    $Self->EventHandlerInit(
70        Config => 'AppointmentCalendar::EventModulePost',
71    );
72
73    $Self->{CacheType} = 'Calendar';
74    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;
75
76    return $Self;
77}
78
79=head2 CalendarCreate()
80
81creates a new calendar for given user.
82
83    my %Calendar = $CalendarObject->CalendarCreate(
84        CalendarName    => 'Meetings',          # (required) Personal calendar name
85        GroupID         => 3,                   # (required) GroupID
86        Color           => '#FF7700',           # (required) Color in hexadecimal RGB notation
87        UserID          => 4,                   # (required) UserID
88
89        TicketAppointments => [                 # (optional) Ticket appointments, array ref of hashes
90            {
91                StartDate => 'FirstResponse',
92                EndDate   => 'Plus_5',
93                QueueID   => [ 2 ],
94                SearchParams => {
95                    Title => 'This is a title',
96                    Types => 'This is a type',
97                },
98            },
99        ],
100
101        ValidID => 1,                   # (optional) Default is 1.
102    );
103
104returns Calendar hash if successful:
105    %Calendar = (
106        CalendarID   => 2,
107        GroupID      => 3,
108        CalendarName => 'Meetings',
109        CreateTime   => '2016-01-01 08:00:00',
110        CreateBy     => 4,
111        ChangeTime   => '2016-01-01 08:00:00',
112        ChangeBy     => 4,
113        ValidID      => 1,
114    );
115
116Events:
117    CalendarCreate
118
119=cut
120
121sub CalendarCreate {
122    my ( $Self, %Param ) = @_;
123
124    # check needed stuff
125    for my $Needed (qw(CalendarName GroupID Color UserID)) {
126        if ( !$Param{$Needed} ) {
127            $Kernel::OM->Get('Kernel::System::Log')->Log(
128                Priority => 'error',
129                Message  => "Need $Needed!",
130            );
131            return;
132        }
133    }
134
135    # check color
136    if ( !( $Param{Color} =~ /#[A-F0-9]{3,6}/i ) ) {
137        $Kernel::OM->Get('Kernel::System::Log')->Log(
138            Priority => 'error',
139            Message  => 'Color must be in hexadecimal RGB notation, eg. #FFFFFF.',
140        );
141        return;
142    }
143
144    # reset ticket appointments
145    if ( !( scalar @{ $Param{TicketAppointments} // [] } ) ) {
146        $Param{TicketAppointments} = undef;
147    }
148
149    # make it uppercase for the sake of consistency
150    $Param{Color} = uc $Param{Color};
151
152    my $ValidID = defined $Param{ValidID} ? $Param{ValidID} : 1;
153
154    my %Calendar = $Self->CalendarGet(
155        CalendarName => $Param{CalendarName},
156    );
157
158    # return if calendar with same name already exists
159    return if %Calendar;
160
161    # create salt string
162    my $SaltString = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
163        Length => 64,
164    );
165
166    # serialize and encode ticket appointment data
167    my $TicketAppointments;
168    if ( $Param{TicketAppointments} ) {
169        $TicketAppointments = $Kernel::OM->Get('Kernel::System::Storable')->Serialize(
170            Data => $Param{TicketAppointments},
171        );
172        $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput($TicketAppointments);
173        $TicketAppointments = MIME::Base64::encode_base64($TicketAppointments);
174    }
175
176    my $SQL = '
177        INSERT INTO calendar
178            (group_id, name, salt_string, color, ticket_appointments, create_time, create_by,
179            change_time, change_by, valid_id)
180        VALUES (?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?, ?)
181    ';
182
183    # create db record
184    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
185        SQL  => $SQL,
186        Bind => [
187            \$Param{GroupID}, \$Param{CalendarName}, \$SaltString, \$Param{Color},
188            \$TicketAppointments, \$Param{UserID}, \$Param{UserID}, \$ValidID
189        ],
190    );
191
192    %Calendar = $Self->CalendarGet(
193        CalendarName => $Param{CalendarName},
194        UserID       => $Param{UserID},
195    );
196    return if !%Calendar;
197
198    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
199
200    # cache value
201    $CacheObject->Set(
202        Type  => $Self->{CacheType},
203        Key   => $Calendar{CalendarID},
204        Value => \%Calendar,
205        TTL   => $Self->{CacheTTL},
206    );
207
208    # reset CalendarList
209    $CacheObject->CleanUp(
210        Type => 'CalendarList',
211    );
212
213    # fire event
214    $Self->EventHandler(
215        Event => 'CalendarCreate',
216        Data  => {
217            %Calendar,
218        },
219        UserID => $Param{UserID},
220    );
221
222    return %Calendar;
223}
224
225=head2 CalendarGet()
226
227Get calendar by name or id.
228
229    my %Calendar = $CalendarObject->CalendarGet(
230        CalendarName => 'Meetings',          # (required) Calendar name
231                                             # or
232        CalendarID   => 4,                   # (required) CalendarID
233
234        UserID       => 2,                   # (optional) UserID - System will check if user has access to calendar if provided
235    );
236
237Returns Calendar data:
238
239    %Calendar = (
240        CalendarID         => 2,
241        GroupID            => 3,
242        CalendarName       => 'Meetings',
243        Color              => '#FF7700',
244        TicketAppointments => [
245            {
246                StartDate => 'FirstResponse',
247                EndDate   => 'Plus_5',
248                QueueID   => [ 2 ],
249                SearchParams => {
250                    Title => 'This is a title',
251                    Types => 'This is a type',
252                },
253            },
254        ],
255        CreateTime => '2016-01-01 08:00:00',
256        CreateBy   => 1,
257        ChangeTime => '2016-01-01 08:00:00',
258        ChangeBy   => 1,
259        ValidID    => 1,
260    );
261
262=cut
263
264sub CalendarGet {
265    my ( $Self, %Param ) = @_;
266
267    # check needed stuff
268    if ( !$Param{CalendarID} && !$Param{CalendarName} ) {
269        $Kernel::OM->Get('Kernel::System::Log')->Log(
270            Priority => 'error',
271            Message  => "Need CalendarID or CalendarName!",
272        );
273        return;
274    }
275
276    my %Calendar;
277
278    if ( $Param{CalendarID} ) {
279
280        # check if value is cached
281        my $Data = $Kernel::OM->Get('Kernel::System::Cache')->Get(
282            Type => $Self->{CacheType},
283            Key  => $Param{CalendarID},
284        );
285
286        if ( IsHashRefWithData($Data) ) {
287            %Calendar = %{$Data};
288        }
289    }
290
291    if ( !%Calendar ) {
292
293        # create db object
294        my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
295
296        my $SQL = '
297            SELECT id, group_id, name, color, ticket_appointments, create_time, create_by,
298            change_time, change_by, valid_id
299            FROM calendar
300            WHERE
301        ';
302
303        my @Bind;
304        if ( $Param{CalendarID} ) {
305            $SQL .= '
306                id=?
307            ';
308            push @Bind, \$Param{CalendarID};
309        }
310        else {
311            $SQL .= '
312                name=?
313            ';
314            push @Bind, \$Param{CalendarName};
315        }
316
317        # db query
318        return if !$DBObject->Prepare(
319            SQL   => $SQL,
320            Bind  => \@Bind,
321            Limit => 1,
322        );
323
324        while ( my @Row = $DBObject->FetchrowArray() ) {
325
326            # decode and deserialize ticket appointment data
327            my $TicketAppointments;
328            if ( $Row[4] ) {
329                my $DecodedData = MIME::Base64::decode_base64( $Row[4] );
330                $TicketAppointments = $Kernel::OM->Get('Kernel::System::Storable')->Deserialize(
331                    Data => $DecodedData,
332                );
333                $TicketAppointments = undef if ref $TicketAppointments ne 'ARRAY';
334            }
335
336            $Calendar{CalendarID}         = $Row[0];
337            $Calendar{GroupID}            = $Row[1];
338            $Calendar{CalendarName}       = $Row[2];
339            $Calendar{Color}              = $Row[3];
340            $Calendar{TicketAppointments} = $TicketAppointments;
341            $Calendar{CreateTime}         = $Row[5];
342            $Calendar{CreateBy}           = $Row[6];
343            $Calendar{ChangeTime}         = $Row[7];
344            $Calendar{ChangeBy}           = $Row[8];
345            $Calendar{ValidID}            = $Row[9];
346        }
347
348        if ( $Param{CalendarID} ) {
349
350            # cache
351            $Kernel::OM->Get('Kernel::System::Cache')->Set(
352                Type  => $Self->{CacheType},
353                Key   => $Param{CalendarID},
354                Value => \%Calendar,
355                TTL   => $Self->{CacheTTL},
356            );
357        }
358
359        if ( $Param{UserID} && $Calendar{GroupID} ) {
360
361            # get user groups
362            my %GroupList = $Kernel::OM->Get('Kernel::System::Group')->PermissionUserGet(
363                UserID => $Param{UserID},
364                Type   => 'ro',
365            );
366
367            if ( !grep { $Calendar{GroupID} == $_ } keys %GroupList ) {
368                %Calendar = ();
369            }
370        }
371    }
372
373    return %Calendar;
374}
375
376=head2 CalendarList()
377
378Get calendar list.
379
380    my @Result = $CalendarObject->CalendarList(
381        UserID     => 4,            # (optional) For permission check
382        Permission => 'rw',         # (optional) Required permission (default ro)
383        ValidID    => 1,            # (optional) Default 0.
384                                    # 0 - All states
385                                    # 1 - All valid
386                                    # 2 - All invalid
387                                    # 3 - All temporary invalid
388    );
389
390Returns:
391
392    @Result = [
393        {
394            CalendarID   => 2,
395            GroupID      => 3,
396            CalendarName => 'Meetings',
397            Color        => '#FF7700',
398            CreateTime   => '2016-01-01 08:00:00',
399            CreateBy     => 3,
400            ChangeTime   => '2016-01-01 08:00:00',
401            ChangeBy     => 3,
402            ValidID      => 1,
403        },
404        {
405            CalendarID   => 3,
406            GroupID      => 3,
407            CalendarName => 'Customer presentations',
408            Color        => '#BB00BB',
409            CreateTime   => '2016-01-01 08:00:00',
410            CreateBy     => 3,
411            ChangeTime   => '2016-01-01 08:00:00',
412            ChangeBy     => 3,
413            ValidID      => 0,
414        },
415        ...
416    ];
417
418=cut
419
420sub CalendarList {
421    my ( $Self, %Param ) = @_;
422
423    # Make different cache type for list (so we can clear cache by this value)
424    my $CacheType     = 'CalendarList';
425    my $CacheKeyUser  = $Param{UserID} || 'all-user-ids';
426    my $CacheKeyValid = $Param{ValidID} || 'all-valid-ids';
427
428    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
429
430    # get cached value if exists
431    my $Data = $CacheObject->Get(
432        Type => $CacheType,
433        Key  => "$CacheKeyUser-$CacheKeyValid",
434    );
435
436    if ( !IsArrayRefWithData($Data) ) {
437
438        # create needed objects
439        my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
440
441        my $SQL = '
442            SELECT id, group_id, name, color, create_time, create_by, change_time, change_by,
443            valid_id
444            FROM calendar
445            WHERE 1=1
446        ';
447        my @Bind;
448
449        if ( $Param{ValidID} ) {
450            $SQL .= ' AND valid_id=? ';
451            push @Bind, \$Param{ValidID};
452        }
453        $SQL .= 'ORDER BY id ASC';
454
455        # db query
456        return if !$DBObject->Prepare(
457            SQL  => $SQL,
458            Bind => \@Bind,
459        );
460
461        my @Result;
462        while ( my @Row = $DBObject->FetchrowArray() ) {
463            my %Calendar;
464            $Calendar{CalendarID}   = $Row[0];
465            $Calendar{GroupID}      = $Row[1];
466            $Calendar{CalendarName} = $Row[2];
467            $Calendar{Color}        = $Row[3];
468            $Calendar{CreateTime}   = $Row[4];
469            $Calendar{CreateBy}     = $Row[5];
470            $Calendar{ChangeTime}   = $Row[6];
471            $Calendar{ChangeBy}     = $Row[7];
472            $Calendar{ValidID}      = $Row[8];
473            push @Result, \%Calendar;
474        }
475
476        # cache data
477        $CacheObject->Set(
478            Type  => $CacheType,
479            Key   => "$CacheKeyUser-$CacheKeyValid",
480            Value => \@Result,
481            TTL   => $Self->{CacheTTL},
482        );
483
484        $Data = \@Result;
485    }
486
487    if ( $Param{UserID} ) {
488
489        # get user groups
490        my %GroupList = $Kernel::OM->Get('Kernel::System::Group')->PermissionUserGet(
491            UserID => $Param{UserID},
492            Type   => $Param{Permission} || 'ro',
493        );
494
495        my @Result;
496
497        for my $Item ( @{$Data} ) {
498            if ( grep { $Item->{GroupID} == $_ } keys %GroupList ) {
499                push @Result, $Item;
500            }
501        }
502
503        $Data = \@Result;
504    }
505
506    return @{$Data};
507}
508
509=head2 CalendarUpdate()
510
511updates an existing calendar.
512
513    my $Success = $CalendarObject->CalendarUpdate(
514        CalendarID       => 1,                   # (required) CalendarID
515        GroupID          => 2,                   # (required) Calendar group
516        CalendarName     => 'Meetings',          # (required) Personal calendar name
517        Color            => '#FF9900',           # (required) Color in hexadecimal RGB notation
518        UserID           => 4,                   # (required) UserID (who made update)
519        ValidID          => 1,                   # (required) ValidID
520
521        TicketAppointments => [                 # (optional) Ticket appointments, array ref of hashes
522            {
523                StartDate => 'FirstResponse',
524                EndDate   => 'Plus_5',
525                QueueID   => [ 2 ],
526                SearchParams => {
527                    Title => 'This is a title',
528                    Types => 'This is a type',
529                },
530            },
531        ],
532    );
533
534Returns 1 if successful.
535
536Events:
537    CalendarUpdate
538
539=cut
540
541sub CalendarUpdate {
542    my ( $Self, %Param ) = @_;
543
544    # check needed stuff
545    for my $Needed (qw(CalendarID GroupID CalendarName Color UserID ValidID)) {
546        if ( !$Param{$Needed} ) {
547            $Kernel::OM->Get('Kernel::System::Log')->Log(
548                Priority => 'error',
549                Message  => "Need $Needed!",
550            );
551            return;
552        }
553    }
554
555    # check color
556    if ( !( $Param{Color} =~ /#[A-F0-9]{3,6}/i ) ) {
557        $Kernel::OM->Get('Kernel::System::Log')->Log(
558            Priority => 'error',
559            Message  => 'Color must be in hexadecimal RGB notation, eg. #FFFFFF.',
560        );
561        return;
562    }
563
564    # reset ticket appointments
565    if ( !( scalar @{ $Param{TicketAppointments} // [] } ) ) {
566        $Param{TicketAppointments} = undef;
567    }
568
569    # make it uppercase for the sake of consistency
570    $Param{Color} = uc $Param{Color};
571
572    # serialize and encode ticket appointment data
573    my $TicketAppointments;
574    if ( $Param{TicketAppointments} ) {
575        $TicketAppointments = $Kernel::OM->Get('Kernel::System::Storable')->Serialize(
576            Data => $Param{TicketAppointments},
577        );
578        $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput($TicketAppointments);
579        $TicketAppointments = MIME::Base64::encode_base64($TicketAppointments);
580    }
581
582    my $SQL = '
583        UPDATE calendar
584        SET group_id=?, name=?, color=?, ticket_appointments=?, change_time=current_timestamp,
585        change_by=?, valid_id=?
586    ';
587
588    my @Bind;
589    push @Bind, \$Param{GroupID}, \$Param{CalendarName}, \$Param{Color}, \$TicketAppointments,
590        \$Param{UserID}, \$Param{ValidID};
591
592    $SQL .= '
593        WHERE id=?
594    ';
595    push @Bind, \$Param{CalendarID};
596
597    # create db record
598    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
599        SQL  => $SQL,
600        Bind => \@Bind,
601    );
602
603    # get cache object
604    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
605
606    # clear cache
607    $CacheObject->CleanUp(
608        Type => 'CalendarList',
609    );
610
611    $CacheObject->Delete(
612        Type => $Self->{CacheType},
613        Key  => $Param{CalendarID},
614    );
615
616    # fire event
617    $Self->EventHandler(
618        Event => 'CalendarUpdate',
619        Data  => {
620            CalendarID => $Param{CalendarID},
621        },
622        UserID => $Param{UserID},
623    );
624
625    return 1;
626}
627
628=head2 CalendarImport()
629
630import a calendar
631
632    my $Success = $CalendarObject->CalendarImport(
633        Data => {
634            CalendarData => {
635                CalendarID   => 2,
636                GroupID      => 3,
637                CalendarName => 'Meetings',
638                Color        => '#FF7700',
639                ValidID      => 1,
640            },
641            AppointmentData => {
642                {
643                    AppointmentID       => 2,
644                    ParentID            => 1,
645                    CalendarID          => 1,
646                    UniqueID            => '20160101T160000-71E386@localhost',
647                    ...
648                },
649                ...
650            },
651        },
652        OverwriteExistingEntities => 0,     # (optional) Overwrite existing calendar and appointments, default: 0
653                                            # Calendar with same name will be overwritten
654                                            # Appointments with same UniqueID in existing calendar will be overwritten
655        UserID => 1,
656    );
657
658returns 1 if successful
659
660=cut
661
662sub CalendarImport {
663    my ( $Self, %Param ) = @_;
664
665    # check needed stuff
666    for my $Needed (qw(Data UserID)) {
667        if ( !$Param{$Needed} ) {
668            $Kernel::OM->Get('Kernel::System::Log')->Log(
669                Priority => 'error',
670                Message  => "Need $Needed!",
671            );
672            return;
673        }
674    }
675
676    return if !IsHashRefWithData( $Param{Data} );
677    return if !IsHashRefWithData( $Param{Data}->{CalendarData} );
678
679    if (
680        defined $Param{Data}->{CalendarData}->{TicketAppointments}
681        && IsArrayRefWithData( $Param{Data}->{CalendarData}->{TicketAppointments} )
682        )
683    {
684        # Get queue create permissions for the user.
685        my %UserGroups = $Kernel::OM->Get('Kernel::System::Group')->PermissionUserGet(
686            UserID => $Param{UserID},
687            Type   => 'create',
688        );
689
690        my @ValidIDs = $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet();
691
692        my $QueueObject = $Kernel::OM->Get('Kernel::System::Queue');
693
694        # Queue field in ticket appointments is mandatory, check if it's present and valid.
695        for my $Rule ( @{ $Param{Data}->{CalendarData}->{TicketAppointments} } ) {
696            if ( defined $Rule->{QueueID} && IsArrayRefWithData( $Rule->{QueueID} ) ) {
697
698                QUEUE_ID:
699                for my $QueueID ( sort @{ $Rule->{QueueID} || [] } ) {
700                    my %QueueData = $QueueObject->QueueGet( ID => $QueueID );
701
702                    if (
703                        !grep { $_ eq $QueueData{ValidID} } @ValidIDs
704                        || !$UserGroups{ $QueueData{GroupID} }
705                        )
706                    {
707                        $Kernel::OM->Get('Kernel::System::Log')->Log(
708                            Priority => 'error',
709                            Message  => "Invalid queue ID $QueueID in ticket appointment rule or no permissions!",
710                        );
711                        return;
712                    }
713                }
714            }
715            else {
716                $Kernel::OM->Get('Kernel::System::Log')->Log(
717                    Priority => 'error',
718                    Message  => 'Need queue ID in ticket appointment rules!',
719                );
720                return;
721            }
722        }
723    }
724
725    # check for an existing calendar
726    my %ExistingCalendar = $Self->CalendarGet(
727        CalendarName => $Param{Data}->{CalendarData}->{CalendarName},
728    );
729
730    my $CalendarID;
731
732    # create new calendar
733    if ( !IsHashRefWithData( \%ExistingCalendar ) ) {
734        my %Calendar = $Self->CalendarCreate(
735            %{ $Param{Data}->{CalendarData} },
736            UserID => $Param{UserID},
737        );
738        return if !$Calendar{CalendarID};
739
740        $CalendarID = $Calendar{CalendarID};
741    }
742
743    # update existing calendar
744    else {
745        if ( $Param{OverwriteExistingEntities} ) {
746            my $Success = $Self->CalendarUpdate(
747                %{ $Param{Data}->{CalendarData} },
748                CalendarID => $ExistingCalendar{CalendarID},
749                UserID     => $Param{UserID},
750            );
751            return if !$Success;
752        }
753
754        $CalendarID = $ExistingCalendar{CalendarID};
755    }
756
757    # import appointments
758    if ( $CalendarID && IsArrayRefWithData( $Param{Data}->{AppointmentData} ) ) {
759        my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
760        my $AppointmentID;
761
762        APPOINTMENT:
763        for my $Appointment ( @{ $Param{Data}->{AppointmentData} } ) {
764
765            # add to existing calendar
766            $Appointment->{CalendarID} = $CalendarID;
767
768            # create new appointment if NOT overwriting existing entities
769            $Appointment->{UniqueID} = undef if !$Param{OverwriteExistingEntities};
770
771            # skip adding automatic recurring occurrences
772            if ( $Appointment->{Recurring} ) {
773                $Appointment->{RecurringRaw} = 1;
774            }
775
776            # set parent id to last appointment id
777            if ( $Appointment->{ParentID} ) {
778                $Appointment->{ParentID} = $AppointmentID;
779            }
780
781            $AppointmentID = $AppointmentObject->AppointmentCreate(
782                %{$Appointment},
783                UserID => $Param{UserID},
784            );
785            return if !$AppointmentID;
786        }
787    }
788
789    return 1;
790}
791
792=head2 CalendarExport()
793
794export a calendar
795
796    my %Data = $CalendarObject->CalendarExport(
797        CalendarID => 2,
798        UserID     => 1,
799    }
800
801returns calendar hash with data:
802
803    %Data = (
804        CalendarData => {
805            CalendarID   => 2,
806            GroupID      => 3,
807            CalendarName => 'Meetings',
808            Color        => '#FF7700',
809            ValidID      => 1,
810        },
811        AppointmentData => (
812            {
813                AppointmentID       => 2,
814                ParentID            => 1,
815                CalendarID          => 1,
816                UniqueID            => '20160101T160000-71E386@localhost',
817                ...
818            },
819            ...
820        ),
821    );
822
823=cut
824
825sub CalendarExport {
826    my ( $Self, %Param ) = @_;
827
828    # check needed stuff
829    for my $Needed (qw(CalendarID UserID)) {
830        if ( !$Param{$Needed} ) {
831            $Kernel::OM->Get('Kernel::System::Log')->Log(
832                Priority => 'error',
833                Message  => "Need $Needed!",
834            );
835            return;
836        }
837    }
838
839    # get calendar data
840    my %CalendarData = $Self->CalendarGet(
841        CalendarID => $Param{CalendarID},
842        UserID     => $Param{UserID},
843    );
844    return if !IsHashRefWithData( \%CalendarData );
845
846    # get appointment object
847    my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
848
849    # get list of appointments
850    my @Appointments = $AppointmentObject->AppointmentList(
851        CalendarID => $Param{CalendarID},
852        Result     => 'ARRAY',
853    );
854
855    my @AppointmentData;
856
857    APPOINTMENTID:
858    for my $AppointmentID (@Appointments) {
859        my %Appointment = $AppointmentObject->AppointmentGet(
860            AppointmentID => $AppointmentID,
861        );
862        next APPOINTMENTID if !%Appointment;
863        next APPOINTMENTID if $Appointment{TicketAppointmentRuleID};
864
865        push @AppointmentData, \%Appointment;
866    }
867
868    my %Result = (
869        CalendarData    => \%CalendarData,
870        AppointmentData => \@AppointmentData,
871    );
872
873    return %Result;
874}
875
876=head2 CalendarPermissionGet()
877
878Get permission level for given CalendarID and UserID.
879
880    my $Permission = $CalendarObject->CalendarPermissionGet(
881        CalendarID  => 1,                   # (required) CalendarID
882        UserID      => 4,                   # (required) UserID
883    );
884
885Returns:
886
887    $Permission = 'rw';    # 'ro', 'rw', ...
888
889=cut
890
891sub CalendarPermissionGet {
892    my ( $Self, %Param ) = @_;
893
894    # check needed stuff
895    for my $Needed (qw(CalendarID UserID)) {
896        if ( !$Param{$Needed} ) {
897            $Kernel::OM->Get('Kernel::System::Log')->Log(
898                Priority => 'error',
899                Message  => "Need $Needed!",
900            );
901            return;
902        }
903    }
904
905    # make sure super user has read/write permission
906    return 'rw' if $Param{UserID} eq 1;
907
908    my %Calendar = $Self->CalendarGet(
909        CalendarID => $Param{CalendarID},
910    );
911
912    my $Result = '';
913
914    my $GroupObject = $Kernel::OM->Get('Kernel::System::Group');
915
916    TYPE:
917    for my $Type (qw(ro move_into create rw)) {
918
919        my %GroupData = $GroupObject->PermissionUserGet(
920            UserID => $Param{UserID},
921            Type   => $Type,
922        );
923
924        if ( $GroupData{ $Calendar{GroupID} } ) {
925            $Result = $Type;
926        }
927        else {
928            last TYPE;
929        }
930    }
931
932    return $Result;
933}
934
935=head2 TicketAppointmentProcessTicket()
936
937Handle the automatic ticket appointments for the ticket.
938
939    $CalendarObject->TicketAppointmentProcessTicket(
940        TicketID => 1,
941    );
942
943This method does not have return value.
944
945=cut
946
947sub TicketAppointmentProcessTicket {
948    my ( $Self, %Param ) = @_;
949
950    # check needed stuff
951    if ( !$Param{TicketID} ) {
952        $Kernel::OM->Get('Kernel::System::Log')->Log(
953            Priority => 'error',
954            Message  => 'Need TicketID!',
955        );
956        return;
957    }
958
959    # get all valid calendars
960    my @Calendars = $Self->CalendarList(
961        ValidID => 1,
962    );
963    return if !@Calendars;
964
965    # get ticket appointment types
966    my %TicketAppointmentTypes = $Self->TicketAppointmentTypesGet();
967
968    # get ticket object
969    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
970
971    # go through all calendars with defined ticket appointments
972    CALENDAR:
973    for my $Calendar (@Calendars) {
974        my %CalendarData = $Self->CalendarGet(
975            CalendarID => $Calendar->{CalendarID},
976        );
977        next CALENDAR if !$CalendarData{TicketAppointments};
978
979        TICKET_APPOINTMENTS:
980        for my $TicketAppointments ( @{ $CalendarData{TicketAppointments} } ) {
981
982            # check appointment types
983            for my $Field (qw(StartDate EndDate)) {
984
985                # allow special time presets for EndDate
986                if ( $Field ne 'EndDate' && !( $TicketAppointments->{$Field} =~ /^Plus_/ ) ) {
987
988                    # skip if ticket appointment type is invalid
989                    if ( !$TicketAppointmentTypes{ $TicketAppointments->{$Field} } ) {
990                        next TICKET_APPOINTMENTS;
991                    }
992                }
993            }
994
995            # check if ticket satisfies the search filter from the ticket appointment rule
996            # pass all configured parameters to ticket search, including ticket id
997            my $Filtered = $TicketObject->TicketSearch(
998                Result   => 'COUNT',
999                TicketID => $Param{TicketID},
1000                QueueIDs => $TicketAppointments->{QueueID},
1001                UserID   => 1,
1002                %{ $TicketAppointments->{SearchParam} // {} },
1003            );
1004
1005            # ticket was found
1006            if ($Filtered) {
1007
1008                # process ticket appointment rule
1009                $Self->TicketAppointmentProcessRule(
1010                    CalendarID => $Calendar->{CalendarID},
1011                    Config     => \%TicketAppointmentTypes,
1012                    Rule       => $TicketAppointments,
1013                    TicketID   => $Param{TicketID},
1014                );
1015            }
1016
1017            # ticket was not found
1018            else {
1019
1020                # remove any existing ticket appointment
1021                $Self->TicketAppointmentDelete(
1022                    CalendarID => $Calendar->{CalendarID},
1023                    TicketID   => $Param{TicketID},
1024                    RuleID     => $TicketAppointments->{RuleID},
1025                );
1026            }
1027        }
1028    }
1029
1030    if ( $Kernel::OM->Get('Kernel::Config')->Get('Debug') ) {
1031        $Kernel::OM->Get('Kernel::System::Log')->Log(
1032            Priority => 'debug',
1033            Message  => "Processed ticket appointments for ticket $Param{TicketID}.",
1034        );
1035    }
1036
1037    return;
1038}
1039
1040=head2 TicketAppointmentProcessCalendar()
1041
1042Handle the automatic ticket appointments for the calendar.
1043
1044    my %Result = $CalendarObject->TicketAppointmentProcessCalendar(
1045        CalendarID => 1,
1046    );
1047
1048Returns log of processed tickets and rules:
1049
1050    %Result = (
1051        Process => [
1052            {
1053                TicketID => 1,
1054                RuleID   => '9bb20ea035e7a9930652a9d82d00c725',
1055                Success  => 1,
1056            },
1057            {
1058                TicketID => 2,
1059                RuleID   => '9bb20ea035e7a9930652a9d82d00c725',
1060                Success  => 1,
1061            },
1062        ],
1063        Cleanup => [
1064            {
1065                RuleID  => 'b272a035ed82d65a927a99300e00c9b5',
1066                Success => 1,
1067            },
1068        ],
1069    );
1070
1071=cut
1072
1073sub TicketAppointmentProcessCalendar {
1074    my ( $Self, %Param ) = @_;
1075
1076    # check needed stuff
1077    if ( !$Param{CalendarID} ) {
1078        $Kernel::OM->Get('Kernel::System::Log')->Log(
1079            Priority => 'error',
1080            Message  => 'Need CalendarID!',
1081        );
1082        return;
1083    }
1084
1085    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
1086
1087    # Get calendar configuration.
1088    my %Calendar = $Self->CalendarGet(
1089        CalendarID => $Param{CalendarID},
1090    );
1091    if ( !%Calendar ) {
1092        $Kernel::OM->Get('Kernel::System::Log')->Log(
1093            Priority => 'error',
1094            Message  => "Could not find calendar $Param{CalendarID}!",
1095        );
1096        return;
1097    }
1098
1099    # Get ticket appointment types
1100    my %TicketAppointmentTypes = $Self->TicketAppointmentTypesGet();
1101
1102    my @Process;
1103    my @Cleanup;
1104    my %RuleIDLookup;
1105
1106    # Check ticket appointments config.
1107    if ( $Calendar{TicketAppointments} && IsArrayRefWithData( $Calendar{TicketAppointments} ) ) {
1108
1109        # Get active rule IDs from the calendar configuration.
1110        %RuleIDLookup = map { $_->{RuleID} => 1 } @{ $Calendar{TicketAppointments} };
1111
1112        TICKET_APPOINTMENTS:
1113        for my $TicketAppointments ( @{ $Calendar{TicketAppointments} } ) {
1114
1115            # Check appointment types.
1116            for my $Field (qw(StartDate EndDate)) {
1117
1118                # Allow special time presets for EndDate.
1119                if ( $Field ne 'EndDate' && !( $TicketAppointments->{$Field} =~ /^Plus_/ ) ) {
1120
1121                    # Skip if ticket appointment type is invalid.
1122                    if ( !$TicketAppointmentTypes{ $TicketAppointments->{$Field} } ) {
1123                        next TICKET_APPOINTMENTS;
1124                    }
1125                }
1126            }
1127
1128            # Get previously created ticket appointments for this rule.
1129            my %OldAppointments = $Self->_TicketAppointmentList(
1130                CalendarID => $Param{CalendarID},
1131                RuleID     => $TicketAppointments->{RuleID},
1132            );
1133
1134            # Find tickets that match search filter
1135            my @TicketIDs = $TicketObject->TicketSearch(
1136                Result   => 'ARRAY',
1137                QueueIDs => $TicketAppointments->{QueueID},
1138                UserID   => 1,
1139                %{ $TicketAppointments->{SearchParam} // {} },
1140            );
1141
1142            # Process each ticket based on ticket appointment rule.
1143            TICKETID:
1144            for my $TicketID ( sort @TicketIDs ) {
1145                my $Success = $Self->TicketAppointmentProcessRule(
1146                    CalendarID => $Param{CalendarID},
1147                    Config     => \%TicketAppointmentTypes,
1148                    Rule       => $TicketAppointments,
1149                    TicketID   => $TicketID,
1150                );
1151
1152                push @Process, {
1153                    TicketID => $TicketID,
1154                    RuleID   => $TicketAppointments->{RuleID},
1155                    Success  => $Success,
1156                };
1157            }
1158
1159            # Remove previously created ticket appointments if they don't match the rule anymore.
1160            OLDTICKETID:
1161            for my $OldTicketID ( sort keys %OldAppointments ) {
1162                next OLDTICKETID if grep { $OldTicketID == $_ } @TicketIDs;
1163
1164                my $Success = $Self->TicketAppointmentDelete(
1165                    AppointmentID => $OldAppointments{$OldTicketID},
1166                    TicketID      => $OldTicketID,
1167                    CalendarID    => $Param{CalendarID},
1168                    RuleID        => $TicketAppointments->{RuleID},
1169                );
1170
1171                push @Cleanup, {
1172                    AppointmentID => $OldAppointments{$OldTicketID},
1173                    TicketID      => $OldTicketID,
1174                    RuleID        => $TicketAppointments->{RuleID},
1175                    Success       => $Success,
1176                };
1177            }
1178        }
1179    }
1180
1181    my @RuleIDs = $Self->TicketAppointmentRuleIDsGet(
1182        CalendarID => $Param{CalendarID},
1183    );
1184
1185    # Remove ticket appointments for missing rules.
1186    for my $RuleID (@RuleIDs) {
1187        if ( !$RuleIDLookup{$RuleID} ) {
1188            my $Success = $Self->TicketAppointmentDelete(
1189                CalendarID => $Param{CalendarID},
1190                RuleID     => $RuleID,
1191            );
1192
1193            push @Cleanup, {
1194                RuleID  => $RuleID,
1195                Success => $Success,
1196            };
1197        }
1198    }
1199
1200    if ( $Kernel::OM->Get('Kernel::Config')->Get('Debug') ) {
1201        $Kernel::OM->Get('Kernel::System::Log')->Log(
1202            Priority => 'debug',
1203            Message  => "Processed ticket appointments for calendar $Param{CalendarID}.",
1204        );
1205    }
1206
1207    return (
1208        Process => \@Process,
1209        Cleanup => \@Cleanup,
1210    );
1211}
1212
1213=head2 TicketAppointmentProcessRule()
1214
1215Process the ticket appointment rule and create, update or delete appointment if necessary.
1216
1217    my $Success = $CalendarObject->TicketAppointmentProcessRule(
1218        CalendarID => 1,
1219        Config => {
1220            DynamicField_TestDate => {
1221                Module => 'Kernel::System::Calendar::Ticket::DynamicField',
1222            },
1223            ...
1224        },
1225        Rule => {
1226            StartDate => 'DynamicField_TestDate',
1227            EndDate   => 'Plus_5',
1228            QueueID   => [ 2 ],
1229            RuleID    => '9bb20ea035e7a9930652a9d82d00c725',
1230            SearchParams => {
1231                Title => 'Welcome*',
1232            },
1233        },
1234        TicketID => 1,
1235    );
1236
1237Returns 1 if successful.
1238
1239=cut
1240
1241sub TicketAppointmentProcessRule {
1242    my ( $Self, %Param ) = @_;
1243
1244    # check needed stuff
1245    for my $Needed (qw(CalendarID Config Rule TicketID)) {
1246        if ( !$Param{$Needed} ) {
1247            $Kernel::OM->Get('Kernel::System::Log')->Log(
1248                Priority => 'error',
1249                Message  => "Need $Needed!",
1250            );
1251            return;
1252        }
1253    }
1254
1255    return if !IsHashRefWithData( $Param{Config} );
1256    return if !IsHashRefWithData( $Param{Rule} );
1257
1258    my $Error;
1259    my $AppointmentType;
1260    my %AppointmentData;
1261
1262    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
1263
1264    # get start and end time values
1265    for my $Field (qw(StartDate EndDate)) {
1266        my $Type = $Param{Rule}->{$Field};
1267
1268        # appointment fields are named differently
1269        my $AppointmentField = $Field;
1270        $AppointmentField =~ s/Date$/Time/;
1271
1272        # check if we are dealing with a registered type
1273        if ( $Param{Config}->{$Type} && $Param{Config}->{$Type}->{Module} ) {
1274            my $GenericModule = $Param{Config}->{$Type}->{Module};
1275
1276            # get the time value via the module method
1277            if ( $MainObject->Require($GenericModule) ) {
1278                $AppointmentData{$AppointmentField} = $GenericModule->new( %{$Self} )->GetTime(
1279                    Type     => $Type,
1280                    TicketID => $Param{TicketID},
1281                );
1282                $Error = 1 if !$AppointmentData{$AppointmentField};
1283            }
1284        }
1285
1286        # time presets are valid only for end time and existing start time
1287        elsif ( $Field eq 'EndDate' && $AppointmentData{StartTime} ) {
1288            if ( $Type =~ /^Plus_([0-9]+)$/ ) {
1289                my $Preset = int $1;
1290
1291                # Get start time.
1292                my $StartTimeObject = $Kernel::OM->Create(
1293                    'Kernel::System::DateTime',
1294                    ObjectParams => {
1295                        String => $AppointmentData{StartTime},
1296                    },
1297                );
1298
1299                # Calculate end time using preset value.
1300                my $EndTimeObject = $StartTimeObject->Clone();
1301                $EndTimeObject->Add(
1302                    Minutes => $Preset,
1303                );
1304
1305                $AppointmentData{EndTime} = $EndTimeObject->ToString();
1306            }
1307            else {
1308                $Error = 1;
1309                $Kernel::OM->Get('Kernel::System::Log')->Log(
1310                    Priority => 'error',
1311                    Message  => "Invalid time preset: $Type",
1312                );
1313            }
1314        }
1315
1316        # unknown type
1317        else {
1318            $Error = 1;
1319        }
1320    }
1321
1322    # Prevent end time before start time.
1323    if ( $AppointmentData{StartTime} && $AppointmentData{EndTime} ) {
1324        my $StartTimeObject = $Kernel::OM->Create(
1325            'Kernel::System::DateTime',
1326            ObjectParams => {
1327                String => $AppointmentData{StartTime},
1328            },
1329        );
1330        my $EndTimeObject = $Kernel::OM->Create(
1331            'Kernel::System::DateTime',
1332            ObjectParams => {
1333                String => $AppointmentData{EndTime},
1334            },
1335        );
1336        if ( $EndTimeObject < $StartTimeObject ) {
1337            $AppointmentData{EndTime} = $AppointmentData{StartTime};
1338        }
1339    }
1340
1341    # get appointment title
1342    if ( !$Error ) {
1343
1344        my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
1345
1346        my $TicketHook        = $ConfigObject->Get('Ticket::Hook');
1347        my $TicketHookDivider = $ConfigObject->Get('Ticket::HookDivider');
1348        my %Ticket            = $Kernel::OM->Get('Kernel::System::Ticket')->TicketGet(
1349            TicketID      => $Param{TicketID},
1350            DynamicFields => 0,
1351            UserID        => 1,
1352        );
1353        $AppointmentData{Title} = "[$TicketHook$TicketHookDivider$Ticket{TicketNumber}] $Ticket{Title}";
1354    }
1355
1356    my $Success;
1357
1358    # check if ticket appointment already exists
1359    my $AppointmentID = $Self->_TicketAppointmentGet(
1360        CalendarID => $Param{CalendarID},
1361        TicketID   => $Param{TicketID},
1362        RuleID     => $Param{Rule}->{RuleID},
1363    );
1364
1365    # ticket appointment was found
1366    if ($AppointmentID) {
1367
1368        # delete the ticket appointment, if error was raised
1369        if ($Error) {
1370            $Success = $Self->TicketAppointmentDelete(
1371                CalendarID    => $Param{CalendarID},
1372                TicketID      => $Param{TicketID},
1373                RuleID        => $Param{Rule}->{RuleID},
1374                AppointmentID => $AppointmentID,
1375            );
1376        }
1377
1378        # update the ticket appointment, otherwise
1379        else {
1380            $Success = $Self->_TicketAppointmentUpdate(
1381                CalendarID    => $Param{CalendarID},
1382                AppointmentID => $AppointmentID,
1383                TicketID      => $Param{TicketID},
1384                RuleID        => $Param{Rule}->{RuleID},
1385                %AppointmentData,
1386            );
1387        }
1388    }
1389
1390    # create ticket appointment if not found
1391    elsif ( !$Error ) {
1392        $Success = $Self->_TicketAppointmentCreate(
1393            CalendarID => $Param{CalendarID},
1394            TicketID   => $Param{TicketID},
1395            RuleID     => $Param{Rule}->{RuleID},
1396            %AppointmentData,
1397        );
1398    }
1399
1400    return $Success;
1401}
1402
1403=head2 TicketAppointmentUpdateTicket()
1404
1405Updates the ticket with data from ticket appointment.
1406
1407    $CalendarObject->TicketAppointmentUpdateTicket(
1408        AppointmentID => 1,
1409        TicketID      => 1,
1410    );
1411
1412This method does not have return value.
1413
1414=cut
1415
1416sub TicketAppointmentUpdateTicket {
1417    my ( $Self, %Param ) = @_;
1418
1419    # check needed stuff
1420    for my $Needed (qw(AppointmentID TicketID)) {
1421        if ( !$Param{$Needed} ) {
1422            $Kernel::OM->Get('Kernel::System::Log')->Log(
1423                Priority => 'error',
1424                Message  => "Need $Needed!",
1425            );
1426            return;
1427        }
1428    }
1429
1430    # get appointment data
1431    my %AppointmentData = $Kernel::OM->Get('Kernel::System::Calendar::Appointment')->AppointmentGet(
1432        AppointmentID => $Param{AppointmentID},
1433    );
1434
1435    # stop if not ticket appointment
1436    return if !$AppointmentData{TicketAppointmentRuleID};
1437
1438    # get ticket appointment rule
1439    my $Rule = $Self->TicketAppointmentRuleGet(
1440        CalendarID => $AppointmentData{CalendarID},
1441        RuleID     => $AppointmentData{TicketAppointmentRuleID},
1442    );
1443    return if !IsHashRefWithData($Rule);
1444
1445    # get ticket appointment types
1446    my %TicketAppointmentTypes = $Self->TicketAppointmentTypesGet();
1447
1448    my $MainObject   = $Kernel::OM->Get('Kernel::System::Main');
1449    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
1450
1451    # process start and end time values
1452    for my $Field (qw(StartDate EndDate)) {
1453        my $Type = $Rule->{$Field};
1454
1455        # appointment fields are named differently
1456        my $AppointmentField = $Field;
1457        $AppointmentField =~ s/Date$/Time/;
1458
1459        # check if we are dealing with a registered type
1460        if ( $TicketAppointmentTypes{$Type} && $TicketAppointmentTypes{$Type}->{Module} ) {
1461            my $GenericModule = $TicketAppointmentTypes{$Type}->{Module};
1462
1463            # set the time value via the module method
1464            if ( $MainObject->Require($GenericModule) ) {
1465
1466                # loop protection: prevent ticket event module from running
1467                $TicketObject->{'_TicketAppointments::AlreadyProcessed'}->{ $Param{TicketID} }++;
1468
1469                my $Success = $GenericModule->new( %{$Self} )->SetTime(
1470                    Type     => $Type,
1471                    Value    => $AppointmentData{$AppointmentField},
1472                    TicketID => $Param{TicketID},
1473                );
1474                if ( !$Success ) {
1475                    $Kernel::OM->Get('Kernel::System::Log')->Log(
1476                        Priority => 'error',
1477                        Message  => "Error setting $Type for ticket $Param{TicketID}!",
1478                    );
1479                }
1480            }
1481        }
1482    }
1483
1484    if ( $Kernel::OM->Get('Kernel::Config')->Get('Debug') ) {
1485        $Kernel::OM->Get('Kernel::System::Log')->Log(
1486            Priority => 'debug',
1487            Message  => "Updated ticket $Param{TicketID} from appointment $Param{AppointmentID}.",
1488        );
1489    }
1490
1491    return;
1492}
1493
1494=head2 TicketAppointmentTicketID()
1495
1496get ticket id of a ticket appointment.
1497
1498    my $TicketID = $CalendarObject->TicketAppointmentTicketID(
1499        AppointmentID => 1,
1500    );
1501
1502returns appointment ID if successful.
1503
1504=cut
1505
1506sub TicketAppointmentTicketID {
1507    my ( $Self, %Param ) = @_;
1508
1509    # check needed stuff
1510    if ( !$Param{AppointmentID} ) {
1511        $Kernel::OM->Get('Kernel::System::Log')->Log(
1512            Priority => 'error',
1513            Message  => 'Need AppointmentID!',
1514        );
1515        return;
1516    }
1517
1518    # get database object
1519    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1520
1521    # db query
1522    return if !$DBObject->Prepare(
1523        SQL => '
1524            SELECT ticket_id
1525            FROM calendar_appointment_ticket
1526            WHERE appointment_id = ?
1527        ',
1528        Bind  => [ \$Param{AppointmentID}, ],
1529        Limit => 1,
1530    );
1531
1532    my $TicketID;
1533    while ( my @Row = $DBObject->FetchrowArray() ) {
1534        $TicketID = $Row[0];
1535    }
1536
1537    return $TicketID;
1538}
1539
1540=head2 TicketAppointmentRuleIDsGet()
1541
1542get used ticket appointment rules for specific calendar.
1543
1544    my @RuleIDs = $CalendarObject->TicketAppointmentRuleIDsGet(
1545        CalendarID => 1,
1546        TicketID   => 1,    # (optional) Return rules used only for specific ticket
1547    );
1548
1549returns array of rule IDs if found.
1550
1551=cut
1552
1553sub TicketAppointmentRuleIDsGet {
1554    my ( $Self, %Param ) = @_;
1555
1556    # check needed stuff
1557    for my $Needed (qw(CalendarID)) {
1558        if ( !$Param{$Needed} ) {
1559            $Kernel::OM->Get('Kernel::System::Log')->Log(
1560                Priority => 'error',
1561                Message  => "Need $Needed!",
1562            );
1563            return;
1564        }
1565    }
1566
1567    my $SQL = '
1568        SELECT rule_id
1569        FROM calendar_appointment_ticket
1570        WHERE calendar_id = ?
1571    ';
1572    my @Bind;
1573    push @Bind, \$Param{CalendarID};
1574
1575    # specific ticket query condition
1576    if ( $Param{TicketID} ) {
1577        $SQL .= '
1578            AND ticket_id = ?
1579        ';
1580        push @Bind, \$Param{TicketID};
1581    }
1582
1583    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1584
1585    # db query
1586    return if !$DBObject->Prepare(
1587        SQL  => $SQL,
1588        Bind => \@Bind,
1589    );
1590
1591    my %RuleIDs;
1592    while ( my @Row = $DBObject->FetchrowArray() ) {
1593        $RuleIDs{ $Row[0] } = 1;
1594    }
1595
1596    # return unique rule ids
1597    return keys %RuleIDs;
1598}
1599
1600=head2 TicketAppointmentRuleGet()
1601
1602get ticket appointment rule.
1603
1604    my $Rule = $CalendarObject->TicketAppointmentRuleGet(
1605        CalendarID => 1,
1606        RuleID     => '9bb20ea035e7a9930652a9d82d00c725',
1607    );
1608
1609returns rule hash:
1610
1611=cut
1612
1613sub TicketAppointmentRuleGet {
1614    my ( $Self, %Param ) = @_;
1615
1616    # check needed stuff
1617    for my $Needed (qw(CalendarID RuleID)) {
1618        if ( !$Param{$Needed} ) {
1619            $Kernel::OM->Get('Kernel::System::Log')->Log(
1620                Priority => 'error',
1621                Message  => "Need $Needed!",
1622            );
1623            return;
1624        }
1625    }
1626
1627    my %Calendar = $Self->CalendarGet(
1628        CalendarID => $Param{CalendarID},
1629    );
1630    return if !$Calendar{TicketAppointments};
1631
1632    my $Result;
1633
1634    RULE:
1635    for my $Rule ( @{ $Calendar{TicketAppointments} || [] } ) {
1636        if ( $Rule->{RuleID} eq $Param{RuleID} ) {
1637            $Result = $Rule;
1638            last RULE;
1639        }
1640    }
1641    return if !$Result;
1642
1643    return $Result;
1644}
1645
1646=head2 TicketAppointmentTypesGet()
1647
1648get defined ticket appointment types from config.
1649
1650    my %TicketAppointmentTypes = $CalendarObject->TicketAppointmentTypesGet();
1651
1652returns hash of appointment types:
1653
1654    %TicketAppointmentTypes = ();
1655
1656=cut
1657
1658sub TicketAppointmentTypesGet {
1659    my ( $Self, %Param ) = @_;
1660
1661    # get ticket appointment types
1662    my $TicketAppointmentConfig = $Kernel::OM->Get('Kernel::Config')->Get('AppointmentCalendar::TicketAppointmentType')
1663        // {};
1664    return if !$TicketAppointmentConfig;
1665
1666    my %TicketAppointmentTypes;
1667
1668    my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField');
1669
1670    TYPE:
1671    for my $TypeKey ( sort keys %{$TicketAppointmentConfig} ) {
1672        next TYPE if !$TicketAppointmentConfig->{$TypeKey}->{Key};
1673
1674        if ( $TypeKey =~ /DynamicField$/ ) {
1675
1676            # get list of all valid date and date/time dynamic fields
1677            my $DynamicFieldList = $DynamicFieldObject->DynamicFieldListGet(
1678                ObjectType => 'Ticket',
1679            );
1680
1681            DYNAMICFIELD:
1682            for my $DynamicField ( @{$DynamicFieldList} ) {
1683                next DYNAMICFIELD if $DynamicField->{FieldType} ne 'Date' && $DynamicField->{FieldType} ne 'DateTime';
1684
1685                my $Key = sprintf( $TicketAppointmentConfig->{$TypeKey}->{Key}, $DynamicField->{Name} );
1686                $TicketAppointmentTypes{$Key} = $TicketAppointmentConfig->{$TypeKey};
1687            }
1688
1689            next TYPE;
1690        }
1691
1692        $TicketAppointmentTypes{ $TicketAppointmentConfig->{$TypeKey}->{Key} } =
1693            $TicketAppointmentConfig->{$TypeKey};
1694    }
1695
1696    return %TicketAppointmentTypes;
1697}
1698
1699=head2 TicketAppointmentDelete()
1700
1701delete ticket appointment(s).
1702
1703    my $Success = $CalendarObject->TicketAppointmentDelete(
1704        CalendarID    => 1,                                     # (required) CalendarID
1705        RuleID        => '9bb20ea035e7a9930652a9d82d00c725',    # (required) RuleID
1706                                                                # or
1707        TicketID      => 1,                                     # (required) Ticket ID
1708
1709        AppointmentID => 1,                                     # (optional) Appointment ID is known
1710    );
1711
1712returns 1 if successful.
1713
1714=cut
1715
1716sub TicketAppointmentDelete {
1717    my ( $Self, %Param ) = @_;
1718
1719    if ( ( !$Param{CalendarID} || !$Param{RuleID} ) && !$Param{TicketID} ) {
1720        $Kernel::OM->Get('Kernel::System::Log')->Log(
1721            Priority => 'error',
1722            Message  => 'Need CalendarID and RuleID, or TicketID!',
1723        );
1724        return;
1725    }
1726
1727    my @AppointmentIDs;
1728    push @AppointmentIDs, $Param{AppointmentID} if $Param{AppointmentID};
1729
1730    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1731
1732    # Appointment ID is unknown.
1733    if ( !@AppointmentIDs ) {
1734        my $SQL = '
1735            SELECT appointment_id
1736            FROM calendar_appointment_ticket
1737            WHERE 1=1
1738        ';
1739        my @Bind;
1740
1741        if ( $Param{CalendarID} && $Param{RuleID} ) {
1742            $SQL .= '
1743                AND calendar_id = ? AND rule_id = ?
1744            ';
1745            push @Bind, \$Param{CalendarID}, \$Param{RuleID};
1746        }
1747
1748        if ( $Param{TicketID} ) {
1749            $SQL .= '
1750                AND ticket_id = ?
1751            ';
1752            push @Bind, \$Param{TicketID};
1753        }
1754
1755        # db query
1756        return if !$DBObject->Prepare(
1757            SQL  => $SQL,
1758            Bind => \@Bind,
1759        );
1760
1761        while ( my @Row = $DBObject->FetchrowArray() ) {
1762            push @AppointmentIDs, $Row[0];
1763        }
1764    }
1765
1766    # Remove the relation(s) from database.
1767    my $SQL = '
1768        DELETE FROM calendar_appointment_ticket
1769        WHERE 1=1
1770    ';
1771    my @Bind;
1772
1773    if ( $Param{CalendarID} && $Param{RuleID} ) {
1774        $SQL .= '
1775            AND calendar_id = ? AND rule_id = ?
1776        ';
1777        push @Bind, \$Param{CalendarID}, \$Param{RuleID};
1778    }
1779
1780    if ( $Param{TicketID} ) {
1781        $SQL .= '
1782            AND ticket_id = ?
1783        ';
1784        push @Bind, \$Param{TicketID};
1785    }
1786
1787    return if !$DBObject->Do(
1788        SQL  => $SQL,
1789        Bind => \@Bind,
1790    );
1791
1792    # get appointment object
1793    my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
1794
1795    # cleanup ticket appointments
1796    APPOINTMENT_ID:
1797    for my $AppointmentID (@AppointmentIDs) {
1798
1799        # check if appointment exists
1800        next APPOINTMENT_ID if !$AppointmentObject->AppointmentGet(
1801            AppointmentID => $AppointmentID,
1802        );
1803
1804        # delete the appointment
1805        return if !$AppointmentObject->AppointmentDelete(
1806            AppointmentID => $AppointmentID,
1807            UserID        => 1,
1808        );
1809    }
1810
1811    return 1;
1812}
1813
1814=head2 GetAccessToken()
1815
1816get access token for the calendar.
1817
1818    my $Token = $CalendarObject->GetAccessToken(
1819        CalendarID => 1,              # (required) CalendarID
1820        UserLogin  => 'agent-1',      # (required) User login
1821    );
1822
1823Returns:
1824
1825    $Token = 'rw';
1826
1827=cut
1828
1829sub GetAccessToken {
1830    my ( $Self, %Param ) = @_;
1831
1832    # check needed stuff
1833    for my $Needed (qw(CalendarID UserLogin)) {
1834        if ( !$Param{$Needed} ) {
1835            $Kernel::OM->Get('Kernel::System::Log')->Log(
1836                Priority => 'error',
1837                Message  => "Need $Needed!",
1838            );
1839            return;
1840        }
1841    }
1842
1843    # create db object
1844    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1845
1846    # db query
1847    return if !$DBObject->Prepare(
1848        SQL   => 'SELECT salt_string FROM calendar WHERE id = ?',
1849        Bind  => [ \$Param{CalendarID} ],
1850        Limit => 1,
1851    );
1852
1853    # fetch the result
1854    my $SaltString;
1855    while ( my @Row = $DBObject->FetchrowArray() ) {
1856        $SaltString = $Row[0];
1857    }
1858
1859    return if !$SaltString;
1860
1861    # Encode user login to UTF8 representation, since MD5 is defined only for strings of bytes.
1862    #   If login contains a Unicode character, MD5 might fail otherwise. Please see bug#12593 for
1863    #   more information.
1864    $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{UserLogin} );
1865
1866    # calculate md5 sum
1867    my $String = "$Param{UserLogin}-$SaltString";
1868    my $MD5    = Digest::MD5->new()->add($String)->hexdigest();
1869
1870    return $MD5;
1871}
1872
1873=head2 GetTextColor()
1874
1875Returns best text color for supplied background, based on luminosity difference algorithm.
1876
1877    my $BestTextColor = $CalendarObject->GetTextColor(
1878        Background => '#FFF',    # (required) must be in valid hexadecimal RGB notation
1879    );
1880
1881Returns:
1882
1883    $BestTextColor = '#000';
1884
1885=cut
1886
1887sub GetTextColor {
1888    my ( $Self, %Param ) = @_;
1889
1890    # check needed stuff
1891    for my $Needed (qw(Background)) {
1892        if ( !$Param{$Needed} ) {
1893            $Kernel::OM->Get('Kernel::System::Log')->Log(
1894                Priority => 'error',
1895                Message  => "Need $Needed!",
1896            );
1897            return;
1898        }
1899    }
1900
1901    # check color
1902    if ( !( $Param{Background} =~ /#[A-F0-9]{3,6}/i ) ) {
1903        $Kernel::OM->Get('Kernel::System::Log')->Log(
1904            Priority => 'error',
1905            Message  => 'Background must be in hexadecimal RGB notation, eg. #FFFFFF.',
1906        );
1907        return;
1908    }
1909
1910    # check if value is cached
1911    my $Data = $Kernel::OM->Get('Kernel::System::Cache')->Get(
1912        Type => $Self->{CacheType} . 'GetTextColor',
1913        Key  => $Param{Background},
1914    );
1915    return $Data if $Data;
1916
1917    # get RGB values
1918    my @BackgroundColor;
1919    my $RGBHex = substr( $Param{Background}, 1 );
1920
1921    # six character hexadecimal string (eg. #FFFFFF)
1922    if ( length $RGBHex == 6 ) {
1923        $BackgroundColor[0] = hex substr( $RGBHex, 0, 2 );
1924        $BackgroundColor[1] = hex substr( $RGBHex, 2, 2 );
1925        $BackgroundColor[2] = hex substr( $RGBHex, 4, 2 );
1926    }
1927
1928    # three character hexadecimal string (eg. #FFF)
1929    elsif ( length $RGBHex == 3 ) {
1930        $BackgroundColor[0] = hex( substr( $RGBHex, 0, 1 ) . substr( $RGBHex, 0, 1 ) );
1931        $BackgroundColor[1] = hex( substr( $RGBHex, 1, 1 ) . substr( $RGBHex, 1, 1 ) );
1932        $BackgroundColor[2] = hex( substr( $RGBHex, 2, 1 ) . substr( $RGBHex, 1, 1 ) );
1933    }
1934
1935    # invalid hexadecimal string
1936    else {
1937        $Kernel::OM->Get('Kernel::System::Log')->Log(
1938            Priority => 'error',
1939            Message  => 'Background must be in valid 3 or 6 character hexadecimal RGB notation, eg. #FFF or #FFFFFF.',
1940        );
1941        return;
1942    }
1943
1944    # predefined text colors
1945    my %TextColors = (
1946        White => [ '255', '255', '255' ],
1947        Gray  => [ '128', '128', '128' ],
1948        Black => [ '0',   '0',   '0' ],
1949    );
1950
1951    # calculate background luminosity
1952    my $BackgroundLum =
1953        0.2126 * ( $BackgroundColor[0] / 255**2.2 ) +
1954        0.7152 * ( $BackgroundColor[1] / 255**2.2 ) +
1955        0.0722 * ( $BackgroundColor[2] / 255**2.2 );
1956
1957    # calculate luminosity difference
1958    my %LumDiff;
1959    for my $TextColor ( sort keys %TextColors ) {
1960        my $TextLum =
1961            0.2126 * ( $TextColors{$TextColor}->[0] / 255**2.2 ) +
1962            0.7152 * ( $TextColors{$TextColor}->[1] / 255**2.2 ) +
1963            0.0722 * ( $TextColors{$TextColor}->[2] / 255**2.2 );
1964
1965        if ( $BackgroundLum > $TextLum ) {
1966            $LumDiff{$TextColor} = ( $BackgroundLum + 0.05 ) / ( $TextLum + 0.05 );
1967        }
1968        else {
1969            $LumDiff{$TextColor} = ( $TextLum + 0.05 ) / ( $BackgroundLum + 0.05 );
1970        }
1971    }
1972
1973    # get maximum luminosity difference
1974    my ($MaxLumDiff) = sort { $b <=> $a } values %LumDiff;
1975    return if !$MaxLumDiff;
1976
1977    # identify best suited color
1978    my ($BestTextColor) = grep { $LumDiff{$_} eq $MaxLumDiff } keys %LumDiff;
1979    return if !$BestTextColor;
1980
1981    # convert to hex string
1982    my $TextColor = sprintf(
1983        '#%X%X%X',
1984        $TextColors{$BestTextColor}->[0],
1985        $TextColors{$BestTextColor}->[1],
1986        $TextColors{$BestTextColor}->[2],
1987    );
1988
1989    # cache
1990    $Kernel::OM->Get('Kernel::System::Cache')->Set(
1991        Type  => $Self->{CacheType} . 'GetTextColor',
1992        Key   => $Param{Background},
1993        Value => $TextColor,
1994        TTL   => $Self->{CacheTTL},
1995    );
1996
1997    return $TextColor;
1998}
1999
2000=begin Internal:
2001
2002=head2 _TicketAppointmentGet()
2003
2004get ticket appointment id if exists.
2005
2006    my $AppointmentID = $CalendarObject->_TicketAppointmentGet(
2007        CalendarID => 1,
2008        TicketID   => 1,
2009        RuleID     => '9bb20ea035e7a9930652a9d82d00c725',
2010    );
2011
2012returns appointment ID if successful.
2013
2014=cut
2015
2016sub _TicketAppointmentGet {
2017    my ( $Self, %Param ) = @_;
2018
2019    # check needed stuff
2020    for my $Needed (qw(CalendarID TicketID RuleID)) {
2021        if ( !$Param{$Needed} ) {
2022            $Kernel::OM->Get('Kernel::System::Log')->Log(
2023                Priority => 'error',
2024                Message  => "Need $Needed!",
2025            );
2026            return;
2027        }
2028    }
2029
2030    # get database object
2031    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
2032
2033    # db query
2034    return if !$DBObject->Prepare(
2035        SQL => '
2036            SELECT appointment_id
2037            FROM calendar_appointment_ticket
2038            WHERE calendar_id = ? AND ticket_id = ? AND rule_id = ?
2039        ',
2040        Bind  => [ \$Param{CalendarID}, \$Param{TicketID}, \$Param{RuleID}, ],
2041        Limit => 1,
2042    );
2043
2044    my $AppointmentID;
2045    while ( my @Row = $DBObject->FetchrowArray() ) {
2046        $AppointmentID = $Row[0];
2047    }
2048
2049    return $AppointmentID;
2050}
2051
2052=head2 _TicketAppointmentList()
2053
2054Get list of ticket appointments based on a rule.
2055
2056    my %Appointments = $CalendarObject->_TicketAppointmentList(
2057        CalendarID => 1,
2058        RuleID     => '9bb20ea035e7a9930652a9d82d00c725',
2059        Key        => 'TicketID',                           # (optional) Return result will be based on this key.
2060                                                                         Default: TicketID, Possible: TicketID|AppointmentID
2061    );
2062
2063Returns list of ticket appointments, where key will be either TicketID (default) or AppointmentID:
2064
2065%Appointments = (
2066    1 => 1,
2067    2 => 2,
2068    ...
2069);
2070
2071=cut
2072
2073sub _TicketAppointmentList {
2074    my ( $Self, %Param ) = @_;
2075
2076    for my $Needed (qw(CalendarID RuleID)) {
2077        if ( !$Param{$Needed} ) {
2078            $Kernel::OM->Get('Kernel::System::Log')->Log(
2079                Priority => 'error',
2080                Message  => "Need $Needed!",
2081            );
2082            return;
2083        }
2084    }
2085
2086    $Param{Key} ||= 'TicketID';
2087
2088    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
2089
2090    return if !$DBObject->Prepare(
2091        SQL => '
2092            SELECT appointment_id, ticket_id
2093            FROM calendar_appointment_ticket
2094            WHERE calendar_id = ? AND rule_id = ?
2095        ',
2096        Bind => [ \$Param{CalendarID}, \$Param{RuleID}, ],
2097    );
2098
2099    my %Result;
2100    while ( my @Row = $DBObject->FetchrowArray() ) {
2101        $Result{ $Row[1] } = $Row[0];
2102    }
2103
2104    if ( $Param{Key} eq 'AppointmentID' ) {
2105        %Result = reverse %Result;
2106    }
2107
2108    return %Result;
2109}
2110
2111=head2 _TicketAppointmentCreate()
2112
2113create ticket appointment.
2114
2115    my $Success = $CalendarObject->_TicketAppointmentCreate(
2116        CalendarID => 1,
2117        TicketID   => 1,
2118        RuleID     => '9bb20ea035e7a9930652a9d82d00c725',
2119        Title      => '[Ticket#20160823810000010] Some Ticket Title',
2120        StartTime  => '2016-08-23 00:00:00',
2121        EndTime    => '2016-08-24 00:00:00',
2122    );
2123
2124returns 1 if successful.
2125
2126=cut
2127
2128sub _TicketAppointmentCreate {
2129    my ( $Self, %Param ) = @_;
2130
2131    # check needed stuff
2132    for my $Needed (qw(CalendarID TicketID RuleID Title StartTime EndTime)) {
2133        if ( !$Param{$Needed} ) {
2134            $Kernel::OM->Get('Kernel::System::Log')->Log(
2135                Priority => 'error',
2136                Message  => "Need $Needed!",
2137            );
2138            return;
2139        }
2140    }
2141
2142    # create appointment
2143    my $AppointmentID = $Kernel::OM->Get('Kernel::System::Calendar::Appointment')->AppointmentCreate(
2144        CalendarID              => $Param{CalendarID},
2145        Title                   => $Param{Title},
2146        StartTime               => $Param{StartTime},
2147        EndTime                 => $Param{EndTime},
2148        TicketAppointmentRuleID => $Param{RuleID},
2149        UserID                  => 1,
2150    );
2151    return if !$AppointmentID;
2152
2153    # save the relation in database
2154    return $Kernel::OM->Get('Kernel::System::DB')->Do(
2155        SQL => '
2156            INSERT INTO calendar_appointment_ticket
2157                (calendar_id, ticket_id, appointment_id, rule_id)
2158            VALUES (?, ?, ?, ?)
2159        ',
2160        Bind => [ \$Param{CalendarID}, \$Param{TicketID}, \$AppointmentID, \$Param{RuleID}, ],
2161    );
2162}
2163
2164=head2 _TicketAppointmentUpdate()
2165
2166update ticket appointment.
2167
2168    my $Success = $CalendarObject->_TicketAppointmentUpdate(
2169        AppointmentID => 1,
2170        TicketID      => 1,
2171        RuleID        => '9bb20ea035e7a9930652a9d82d00c725',
2172        Title         => '[Ticket#20160823810000010] Some Ticket Title',
2173        StartTime     => '2016-08-23 00:00:00',
2174        EndTime       => '2016-08-24 00:00:00',
2175    );
2176
2177returns 1 if successful.
2178
2179=cut
2180
2181sub _TicketAppointmentUpdate {
2182    my ( $Self, %Param ) = @_;
2183
2184    # check needed stuff
2185    for my $Needed (qw(AppointmentID TicketID RuleID Title StartTime EndTime)) {
2186        if ( !$Param{$Needed} ) {
2187            $Kernel::OM->Get('Kernel::System::Log')->Log(
2188                Priority => 'error',
2189                Message  => "Need $Needed!",
2190            );
2191            return;
2192        }
2193    }
2194
2195    # get appointment object
2196    my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
2197
2198    # get current ticket appointment data
2199    my %Appointment = $AppointmentObject->AppointmentGet(
2200        AppointmentID => $Param{AppointmentID},
2201    );
2202
2203    # ticket appointment does not exist
2204    if ( !$Appointment{AppointmentID} ) {
2205
2206        # remove the relation as well
2207        $Self->TicketAppointmentDelete(
2208            %Param,
2209        );
2210
2211        # create new ticket appointment
2212        return $Self->_TicketAppointmentCreate(
2213            %Param,
2214        );
2215    }
2216
2217    # loop protection: prevent appointment event module from running
2218    $Self->{'_TicketAppointments::TicketUpdate'}->{ $Appointment{AppointmentID} }++;
2219
2220    # update ticket appointment
2221    return $AppointmentObject->AppointmentUpdate(
2222        %Appointment,
2223        Title                   => $Param{Title},
2224        StartTime               => $Param{StartTime},
2225        EndTime                 => $Param{EndTime},
2226        TicketAppointmentRuleID => $Param{RuleID},
2227        UserID                  => 1,
2228    );
2229}
2230
22311;
2232
2233=end Internal:
2234
2235=head1 TERMS AND CONDITIONS
2236
2237This software is part of the OTRS project (L<https://otrs.org/>).
2238
2239This software comes with ABSOLUTELY NO WARRANTY. For details, see
2240the enclosed file COPYING for license information (GPL). If you
2241did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
2242
2243=cut
2244