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::CalendarTemplateGenerator;
10
11use strict;
12use warnings;
13
14use Kernel::Language;
15
16use Kernel::System::VariableCheck qw(:all);
17
18our @ObjectDependencies = (
19    'Kernel::Config',
20    'Kernel::System::Calendar',
21    'Kernel::System::Calendar::Appointment',
22    'Kernel::System::DateTime',
23    'Kernel::System::HTMLUtils',
24    'Kernel::System::Log',
25    'Kernel::System::User',
26    'Kernel::System::Main',
27    'Kernel::System::Group',
28    'Kernel::System::Valid',
29);
30
31=head1 NAME
32
33Kernel::System::CalendarTemplateGenerator - signature lib
34
35=head1 DESCRIPTION
36
37All signature functions.
38
39=head1 PUBLIC INTERFACE
40
41=head2 new()
42
43create an object. Do not use it directly, instead use:
44
45    use Kernel::System::ObjectManager;
46    local $Kernel::OM = Kernel::System::ObjectManager->new();
47    my $TemplateGeneratorObject = $Kernel::OM->Get('Kernel::System::TemplateGenerator');
48
49=cut
50
51sub new {
52    my ( $Type, %Param ) = @_;
53
54    # allocate new hash for object
55    my $Self = {};
56    bless( $Self, $Type );
57
58    $Self->{RichText} = $Kernel::OM->Get('Kernel::Config')->Get('Frontend::RichText');
59
60    return $Self;
61}
62
63=head2 NotificationEvent()
64
65replace all OTRS smart tags in the notification body and subject
66
67    my %NotificationEvent = $CalendarTemplateGeneratorObject->NotificationEvent(
68        AppointmentID => 123,
69        Recipient     => $UserDataHashRef,          # Agent data get result
70        Notification  => $NotificationDataHashRef,
71        UserID        => 123,
72    );
73
74=cut
75
76sub NotificationEvent {
77    my ( $Self, %Param ) = @_;
78
79    # check needed stuff
80    for my $Needed (qw(Notification Recipient UserID)) {
81        if ( !$Param{$Needed} ) {
82            $Kernel::OM->Get('Kernel::System::Log')->Log(
83                Priority => 'error',
84                Message  => "Need $Needed!",
85            );
86            return;
87        }
88    }
89
90    if ( !IsHashRefWithData( $Param{Notification} ) ) {
91        $Kernel::OM->Get('Kernel::System::Log')->Log(
92            Priority => 'error',
93            Message  => "Notification is invalid!",
94        );
95        return;
96    }
97
98    my %Notification = %{ $Param{Notification} };
99
100    # get system default language
101    my $DefaultLanguage = $Kernel::OM->Get('Kernel::Config')->Get('DefaultLanguage') || 'en';
102
103    my $Languages = [ $Param{Recipient}->{UserLanguage}, $DefaultLanguage, 'en' ];
104
105    my $Language;
106    LANGUAGE:
107    for my $Item ( @{$Languages} ) {
108        next LANGUAGE if !$Item;
109        next LANGUAGE if !$Notification{Message}->{$Item};
110
111        # set language
112        $Language = $Item;
113        last LANGUAGE;
114    }
115
116    # if no language, then take the first one available
117    if ( !$Language ) {
118        my @NotificationLanguages = sort keys %{ $Notification{Message} };
119        $Language = $NotificationLanguages[0];
120    }
121
122    # copy the correct language message attributes to a flat structure
123    for my $Attribute (qw(Subject Body ContentType)) {
124        $Notification{$Attribute} = $Notification{Message}->{$Language}->{$Attribute};
125    }
126
127    my $Start = '<';
128    my $End   = '>';
129    if ( $Notification{ContentType} =~ m{text\/html} ) {
130        $Start = '&lt;';
131        $End   = '&gt;';
132    }
133
134    # get html utils object
135    my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils');
136
137    # do text/plain to text/html convert
138    if ( $Self->{RichText} && $Notification{ContentType} =~ /text\/plain/i ) {
139        $Notification{ContentType} = 'text/html';
140        $Notification{Body}        = $HTMLUtilsObject->ToHTML(
141            String => $Notification{Body},
142        );
143    }
144
145    # do text/html to text/plain convert
146    if ( !$Self->{RichText} && $Notification{ContentType} =~ /text\/html/i ) {
147        $Notification{ContentType} = 'text/plain';
148        $Notification{Body}        = $HTMLUtilsObject->ToAscii(
149            String => $Notification{Body},
150        );
151    }
152
153    # get notify texts
154    for my $Text (qw(Subject Body)) {
155        if ( !$Notification{$Text} ) {
156            $Notification{$Text} = "No Notification $Text for $Param{Type} found!";
157        }
158    }
159
160    # replace place holder stuff
161    $Notification{Body} = $Self->_Replace(
162        RichText      => $Self->{RichText},
163        Text          => $Notification{Body},
164        Recipient     => $Param{Recipient},
165        AppointmentID => $Param{AppointmentID},
166        CalendarID    => $Param{CalendarID},
167        UserID        => $Param{UserID},
168        Language      => $Language,
169    );
170
171    $Notification{Subject} = $Self->_Replace(
172        RichText      => 0,
173        Text          => $Notification{Subject},
174        Recipient     => $Param{Recipient},
175        AppointmentID => $Param{AppointmentID},
176        CalendarID    => $Param{CalendarID},
177        UserID        => $Param{UserID},
178        Language      => $Language,
179    );
180
181    # add URLs and verify to be full HTML document
182    if ( $Self->{RichText} ) {
183
184        $Notification{Body} = $HTMLUtilsObject->LinkQuote(
185            String => $Notification{Body},
186        );
187    }
188
189    return %Notification;
190}
191
192=begin Internal:
193
194=cut
195
196sub _Replace {
197    my ( $Self, %Param ) = @_;
198
199    # check needed stuff
200    for (qw(Text RichText UserID)) {
201        if ( !defined $Param{$_} ) {
202            $Kernel::OM->Get('Kernel::System::Log')->Log(
203                Priority => 'error',
204                Message  => "Need $_!"
205            );
206            return;
207        }
208    }
209
210    # check for mailto links
211    # since the subject and body of those mailto links are
212    # uri escaped we have to uri unescape them, replace
213    # possible placeholders and then re-uri escape them
214    $Param{Text} =~ s{
215        (href="mailto:[^\?]+\?)([^"]+")
216    }
217    {
218        my $MailToHref        = $1;
219        my $MailToHrefContent = $2;
220
221        $MailToHrefContent =~ s{
222            ((?:subject|body)=)(.+?)("|&)
223        }
224        {
225            my $SubjectOrBodyPrefix  = $1;
226            my $SubjectOrBodyContent = $2;
227            my $SubjectOrBodySuffix  = $3;
228
229            my $SubjectOrBodyContentUnescaped = URI::Escape::uri_unescape $SubjectOrBodyContent;
230
231            my $SubjectOrBodyContentReplaced = $Self->_Replace(
232                %Param,
233                Text     => $SubjectOrBodyContentUnescaped,
234                RichText => 0,
235            );
236
237            my $SubjectOrBodyContentEscaped = URI::Escape::uri_escape_utf8 $SubjectOrBodyContentReplaced;
238
239            $SubjectOrBodyPrefix . $SubjectOrBodyContentEscaped . $SubjectOrBodySuffix;
240        }egx;
241
242        $MailToHref . $MailToHrefContent;
243    }egx;
244
245    my $Start = '<';
246    my $End   = '>';
247    if ( $Param{RichText} ) {
248        $Start = '&lt;';
249        $End   = '&gt;';
250        $Param{Text} =~ s/(\n|\r)//g;
251    }
252
253    # get needed objects
254    my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
255    my $CalendarObject    = $Kernel::OM->Get('Kernel::System::Calendar');
256
257    my %Calendar;
258    my %Appointment;
259
260    if ( $Param{AppointmentID} ) {
261        %Appointment = $AppointmentObject->AppointmentGet(
262            AppointmentID => $Param{AppointmentID},
263        );
264        %Calendar = $CalendarObject->CalendarGet(
265            CalendarID => $Appointment{CalendarID} || $Param{CalendarID},
266        );
267    }
268    elsif ( $Param{CalendarID} ) {
269        %Calendar = $CalendarObject->CalendarGet(
270            CalendarID => $Param{CalendarID},
271        );
272    }
273
274    # get config object
275    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
276
277    # special replace from secret config options
278    my @SecretConfigOptions = qw(
279        DatabasePw
280        SearchUserPw
281        UserPw
282        SendmailModule::AuthPassword
283        AuthModule::Radius::Password
284        PGP::Key::Password
285        Customer::AuthModule::DB::CustomerPassword
286        Customer::AuthModule::Radius::Password
287        PublicFrontend::AuthPassword
288    );
289
290    # replace the secret config options before the normal config options
291    for my $SecretConfigOption (@SecretConfigOptions) {
292
293        my $Tag = $Start . 'OTRS_CONFIG_' . $SecretConfigOption . $End;
294        $Param{Text} =~ s{$Tag}{xxx}gx;
295    }
296
297    # replace config options
298    my $Tag = $Start . 'OTRS_CONFIG_';
299    $Param{Text} =~ s{$Tag(.+?)$End}{$ConfigObject->Get($1) // ''}egx;
300
301    # cleanup
302    $Param{Text} =~ s/$Tag.+?$End/-/gi;
303
304    my %Recipient = %{ $Param{Recipient} || {} };
305
306    # get user object
307    my $UserObject = $Kernel::OM->Get('Kernel::System::User');
308
309    if ( !%Recipient && $Param{RecipientID} ) {
310
311        %Recipient = $UserObject->GetUserData(
312            UserID        => $Param{RecipientID},
313            NoOutOfOffice => 1,
314        );
315    }
316
317    # Instantiate a new language object with the given language.
318    my $LanguageObject = Kernel::Language->new(
319        UserLanguage => $Param{Language} || 'en',
320    );
321
322    # supported appointment fields
323    my %AppointmentTagsSkip = (
324        ParentID                              => 1,
325        RecurrenceType                        => 1,
326        RecurrenceFrequency                   => 1,
327        RecurrenceCount                       => 1,
328        RecurrenceInterval                    => 1,
329        RecurrenceUntil                       => 1,
330        RecurrenceID                          => 1,
331        RecurrenceExclude                     => 1,
332        NotificationCustom                    => 1,
333        NotificationTemplate                  => 1,
334        NotificationCustomUnitCount           => 1,
335        NotificationCustomUnit                => 1,
336        NotificationCustomUnitPointOfTime     => 1,
337        NotificationCustomRelativePointOfTime => 1,
338        NotificationCustomRelativeUnit        => 1,
339        NotificationCustomRelativeUnitCount   => 1,
340        NotificationCustomDateTime            => 1,
341    );
342
343    # ------------------------------------------------------------ #
344    # process appointment
345    # ------------------------------------------------------------ #
346
347    # replace config options
348    $Tag = $Start . 'OTRS_APPOINTMENT_';
349
350    # replace appointment tags
351    ATTRIBUTE:
352    for my $Attribute ( sort keys %Appointment ) {
353
354        next ATTRIBUTE if !$Attribute;
355        next ATTRIBUTE if $AppointmentTagsSkip{$Attribute};
356
357        # setup a new tag for the current attribute
358        my $MatchTag = $Tag . uc $Attribute;
359
360        # map NotificationTime attribute
361        if ( $Attribute eq 'NotificationDate' ) {
362            $MatchTag = $Tag . 'NOTIFICATIONTIME';
363        }
364
365        my $Replacement = '';
366
367        # process datetime strings (timestamps)
368        if (
369            $Attribute eq 'StartTime'
370            || $Attribute eq 'EndTime'
371            || $Attribute eq 'NotificationDate'
372            || $Attribute eq 'CreateTime'
373            || $Attribute eq 'ChangeTime'
374            )
375        {
376            if ( !$Appointment{$Attribute} ) {
377                $Replacement = '-';
378            }
379            else {
380
381                # Get the stored date time.
382                my $TagSystemTimeObject = $Kernel::OM->Create(
383                    'Kernel::System::DateTime',
384                    ObjectParams => {
385                        String => $Appointment{$Attribute},
386                    },
387                );
388
389                # Convert to recipients time zone.
390                $TagSystemTimeObject->ToTimeZone(
391                    TimeZone => $Recipient{UserTimeZone}
392                        // $TagSystemTimeObject->UserDefaultTimeZoneGet(),
393                );
394
395                my $DateFormat = 'DateFormat';
396
397                # Do not include time component for all-day appointments,
398                #   but only for start and end dates.
399                if (
400                    $Appointment{AllDay}
401                    && (
402                        $Attribute eq 'StartTime'
403                        || $Attribute eq 'EndTime'
404                    )
405                    )
406                {
407                    $DateFormat .= 'Short';
408                }
409
410                # Prepare dates and times.
411                $Replacement = $LanguageObject->FormatTimeString( $TagSystemTimeObject->ToString(), $DateFormat ) || '';
412            }
413        }
414
415        # process createby and changeby
416        elsif ( $Attribute eq 'CreateBy' || $Attribute eq 'ChangeBy' ) {
417
418            $Replacement = $UserObject->UserName(
419                UserID => $Appointment{$Attribute},
420            );
421        }
422
423        # process team ids
424        elsif ( $Attribute eq 'TeamID' ) {
425
426            next ATTRIBUTE if !IsArrayRefWithData( $Appointment{$Attribute} );
427
428            if (
429                !$Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::Calendar::Team', Silent => 1 )
430                )
431            {
432                next ATTRIBUTE;
433            }
434
435            # instanciate a new team object
436            my $TeamObject = Kernel::System::Calendar::Team->new();
437
438            # get a list of available (readable) teams
439            my %TeamList = $TeamObject->TeamList(
440                Valid  => 0,
441                UserID => $Self->{UserID},
442            );
443
444            next ATTRIBUTE if !IsHashRefWithData( \%TeamList );
445
446            my @TeamNames;
447
448            if ( IsHashRefWithData( \%TeamList ) ) {
449
450                TEAMKEY:
451                for my $TeamKey ( @{ $Appointment{$Attribute} } ) {
452
453                    next TEAMKEY if !$TeamList{$TeamKey};
454
455                    push @TeamNames, $TeamList{$TeamKey};
456                }
457            }
458
459            next ATTRIBUTE if !IsArrayRefWithData( \@TeamNames );
460
461            # replace team ids with a comma seperated list of team names
462            $Replacement = join ', ', @TeamNames;
463        }
464
465        # process resource ids
466        elsif ( $Attribute eq 'ResourceID' ) {
467
468            next ATTRIBUTE if !IsArrayRefWithData( $Appointment{$Attribute} );
469
470            my @UserNames;
471
472            USERID:
473            for my $UserID ( @{ $Appointment{$Attribute} } ) {
474
475                next USERID if !$UserID;
476
477                my $UserName = $UserObject->UserName(
478                    UserID => $UserID,
479                );
480
481                next USERID if !$UserName;
482
483                push @UserNames, $UserName;
484            }
485
486            next ATTRIBUTE if !IsArrayRefWithData( \@UserNames );
487
488            # replace resource ids with a comma seperated list of team names
489            $Replacement = join ', ', @UserNames;
490        }
491
492        # process all day and recurring tags
493        elsif (
494            $Attribute eq 'AllDay'
495            || $Attribute eq 'Recurring'
496            )
497        {
498            my $TranslatedString = $LanguageObject->Translate('No');
499
500            if ( $Appointment{$Attribute} ) {
501                $TranslatedString = $LanguageObject->Translate('Yes');
502            }
503
504            $Replacement = $TranslatedString;
505        }
506
507        # process description tag
508        elsif (
509            $Attribute eq 'Description'
510            && $Self->{RichText}
511            && $Appointment{$Attribute}
512            )
513        {
514            my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils');
515            $Replacement = $HTMLUtilsObject->ToHTML(
516                String => $Appointment{$Attribute},
517            );
518        }
519
520        # process all other single values
521        else {
522            if ( !$Appointment{$Attribute} ) {
523                $Replacement = '-';
524            }
525            else {
526                $Replacement = $Appointment{$Attribute};
527            }
528        }
529
530        $Replacement ||= '';
531
532        # replace the tags
533        $Param{Text} =~ s{$MatchTag$End}{$Replacement}egx;
534    }
535
536    # cleanup
537    $Param{Text} =~ s/$Tag.+?$End/-/gi;
538
539    # ------------------------------------------------------------ #
540    # process calendar
541    # ------------------------------------------------------------ #
542
543    # replace config options
544    $Tag = $Start . 'OTRS_CALENDAR_';
545
546    # replace appointment tags
547    ATTRIBUTE:
548    for my $Attribute ( sort keys %Calendar ) {
549
550        next ATTRIBUTE if !$Attribute;
551
552        # setup a new tag for the current attribute
553        my $MatchTag = $Tag . uc $Attribute;
554
555        my $Replacement = '';
556
557        # process datetime strings (timestamps)
558        if ( $Attribute eq 'CreateTime' || $Attribute eq 'ChangeTime' ) {
559
560            # Get the stored date time.
561            my $TagSystemTimeObject = $Kernel::OM->Create(
562                'Kernel::System::DateTime',
563                ObjectParams => {
564                    String => $Calendar{$Attribute},
565                },
566            );
567
568            # Convert to recipients time zone.
569            $TagSystemTimeObject->ToTimeZone(
570                TimeZone => $Recipient{UserTimeZone}
571                    // $TagSystemTimeObject->UserDefaultTimeZoneGet(),
572            );
573
574            # Prepare dates and times.
575            $Replacement = $LanguageObject->FormatTimeString( $TagSystemTimeObject->ToString(), 'DateFormat' ) || '';
576        }
577
578        # process createby and changeby
579        elsif ( $Attribute eq 'CreateBy' || $Attribute eq 'ChangeBy' ) {
580
581            $Replacement = $UserObject->UserName(
582                UserID => $Calendar{$Attribute},
583            );
584        }
585
586        # process group id
587        elsif ( $Attribute eq 'GroupID' ) {
588            $Replacement = $Kernel::OM->Get('Kernel::System::Group')->GroupLookup(
589                GroupID => $Calendar{$Attribute},
590            );
591        }
592
593        # process valid id
594        elsif ( $Attribute eq 'ValidID' ) {
595
596            $Replacement = $Kernel::OM->Get('Kernel::System::Valid')->ValidLookup(
597                ValidID => $Calendar{$Attribute},
598            );
599            $Replacement = $LanguageObject->Translate($Replacement);
600        }
601
602        # process all other single values
603        else {
604            if ( !$Calendar{$Attribute} ) {
605                $Replacement = '-';
606            }
607            else {
608                $Replacement = $Calendar{$Attribute};
609            }
610        }
611
612        # replace the tags
613        $Param{Text} =~ s{$MatchTag$End}{$Replacement}egx;
614    }
615
616    # cleanup
617    $Param{Text} =~ s/$Tag.+?$End/-/gi;
618
619    my $HashGlobalReplace = sub {
620        my ( $Tag, %H ) = @_;
621
622        # Generate one single matching string for all keys to save performance.
623        my $Keys = join '|', map {quotemeta} grep { defined $H{$_} } keys %H;
624
625        # Add all keys also as lowercase to be able to match case insensitive,
626        #   e. g. <OTRS_CUSTOMER_From> and <OTRS_CUSTOMER_FROM>.
627        for my $Key ( sort keys %H ) {
628            $H{ lc $Key } = $H{$Key};
629        }
630
631        $Param{Text} =~ s/(?:$Tag)($Keys)$End/$H{ lc $1 }/ieg;
632    };
633
634    # get recipient data and replace it with <OTRS_...
635    $Tag = $Start . 'OTRS_';
636
637    # include more readable tag <OTRS_NOTIFICATION_RECIPIENT
638    my $RecipientTag = $Start . 'OTRS_NOTIFICATION_RECIPIENT_';
639
640    if (%Recipient) {
641
642        # HTML quoting of content
643        if ( $Param{RichText} ) {
644            ATTRIBUTE:
645            for my $Attribute ( sort keys %Recipient ) {
646                next ATTRIBUTE if !$Recipient{$Attribute};
647                $Recipient{$Attribute} = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToHTML(
648                    String => $Recipient{$Attribute},
649                );
650            }
651        }
652
653        $HashGlobalReplace->( "$Tag|$RecipientTag", %Recipient );
654    }
655
656    # cleanup
657    $Param{Text} =~ s/$RecipientTag.+?$End/-/gi;
658
659    return $Param{Text};
660}
661
6621;
663
664=end Internal:
665
666=head1 TERMS AND CONDITIONS
667
668This software is part of the OTRS project (L<https://otrs.org/>).
669
670This software comes with ABSOLUTELY NO WARRANTY. For details, see
671the enclosed file COPYING for license information (GPL). If you
672did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
673
674=cut
675