# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::Output::HTML::Layout; use strict; use warnings; use URI::Escape qw(); use Digest::MD5 qw(md5_hex); use Kernel::System::VariableCheck qw(:all); use Kernel::Language qw(Translatable); our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::Language', 'Kernel::System::AuthSession', 'Kernel::System::Cache', 'Kernel::System::Chat', 'Kernel::System::CustomerGroup', 'Kernel::System::DateTime', 'Kernel::System::Group', 'Kernel::System::Encode', 'Kernel::System::HTMLUtils', 'Kernel::System::JSON', 'Kernel::System::Log', 'Kernel::System::Main', 'Kernel::System::OTRSBusiness', 'Kernel::System::State', 'Kernel::System::Storable', 'Kernel::System::SystemMaintenance', 'Kernel::System::User', 'Kernel::System::VideoChat', 'Kernel::System::Web::Request', ); =head1 NAME Kernel::Output::HTML::Layout - all generic html functions =head1 DESCRIPTION All generic html functions. E. g. to get options fields, template processing, ... =head1 PUBLIC INTERFACE =head2 new() create a new object. Do not use it directly, instead use: use Kernel::System::ObjectManager; local $Kernel::OM = Kernel::System::ObjectManager->new( 'Kernel::Output::HTML::Layout' => { Lang => 'de', }, ); my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout'); From the web installer, a special Option C is passed to indicate that a database connection is not yet available. use Kernel::System::ObjectManager; local $Kernel::OM = Kernel::System::ObjectManager->new( 'Kernel::Output::HTML::Layout' => { InstallerOnly => 1, }, ); my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout'); =cut sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {%Param}; bless( $Self, $Type ); # set debug $Self->{Debug} = 0; # reset block data delete $Self->{BlockData}; # empty action if not defined $Self->{Action} = '' if !defined $Self->{Action}; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # get/set some common params if ( !$Self->{UserTheme} ) { $Self->{UserTheme} = $ConfigObject->Get('DefaultTheme'); } $Self->{UserTimeZone} ||= Kernel::System::DateTime->UserDefaultTimeZoneGet(); # Determine the language to use based on the browser setting, if there # is none yet. if ( !$Self->{UserLanguage} ) { my @BrowserLanguages = split /\s*,\s*/, $Self->{Lang} || $ENV{HTTP_ACCEPT_LANGUAGE} || ''; my %Data = %{ $ConfigObject->Get('DefaultUsedLanguages') }; LANGUAGE: for my $BrowserLang (@BrowserLanguages) { for my $Language ( reverse sort keys %Data ) { # check xx_XX and xx-XX type my $LanguageOtherType = $Language; $LanguageOtherType =~ s/_/-/; if ( $BrowserLang =~ /^($Language|$LanguageOtherType)/i ) { $Self->{UserLanguage} = $Language; last LANGUAGE; } } if ( !$Self->{UserLanguage} ) { for my $Language ( reverse sort keys %Data ) { # If Browser requests 'vi', also offer 'vi_VI' even though we don't have 'vi' if ( $Language =~ m/^$BrowserLang/smxi ) { $Self->{UserLanguage} = $Language; last LANGUAGE; } } } } $Self->{UserLanguage} ||= $ConfigObject->Get('DefaultLanguage') || 'en'; } # create language object if ( !$Self->{LanguageObject} ) { $Kernel::OM->ObjectParamAdd( 'Kernel::Language' => { UserTimeZone => $Self->{UserTimeZone}, UserLanguage => $Self->{UserLanguage}, Action => $Self->{Action}, }, ); $Self->{LanguageObject} = $Kernel::OM->Get('Kernel::Language'); } # set charset if there is no charset given $Self->{UserCharset} = 'utf-8'; $Self->{Charset} = $Self->{UserCharset}; # just for compat. $Self->{SessionID} = $Param{SessionID} || ''; $Self->{SessionName} = $Param{SessionName} || 'SessionID'; $Self->{CGIHandle} = $ENV{SCRIPT_NAME} || 'No-$ENV{"SCRIPT_NAME"}'; # baselink $Self->{Baselink} = $Self->{CGIHandle} . '?'; $Self->{Time} = $Self->{LanguageObject}->Time( Action => 'GET', Format => 'DateFormat', ); $Self->{TimeLong} = $Self->{LanguageObject}->Time( Action => 'GET', Format => 'DateFormatLong', ); # set text direction $Self->{TextDirection} = $Self->{LanguageObject}->{TextDirection}; # check Frontend::Output::FilterElementPost $Self->{FilterElementPost} = {}; my %FilterElementPost = %{ $ConfigObject->Get('Frontend::Output::FilterElementPost') // {} }; FILTER: for my $Filter ( sort keys %FilterElementPost ) { # extract filter config my $FilterConfig = $FilterElementPost{$Filter}; next FILTER if !$FilterConfig || ref $FilterConfig ne 'HASH'; # extract template list my %TemplateList = %{ $FilterConfig->{Templates} || {} }; if ( !%TemplateList || $TemplateList{ALL} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => <{Module} will be ignored because it wants to operate on all templates or does not specify a template list. EOF ); next FILTER; } $Self->{FilterElementPost}->{$Filter} = $FilterElementPost{$Filter}; } # check Frontend::Output::FilterContent $Self->{FilterContent} = $ConfigObject->Get('Frontend::Output::FilterContent'); # check Frontend::Output::FilterText $Self->{FilterText} = $ConfigObject->Get('Frontend::Output::FilterText'); # check browser $Self->{Browser} = 'Unknown'; $Self->{BrowserVersion} = 0; $Self->{Platform} = ''; $Self->{IsMobile} = 0; $Self->{BrowserJavaScriptSupport} = 1; $Self->{BrowserRichText} = 1; my $HttpUserAgent = ( defined $ENV{HTTP_USER_AGENT} ? lc $ENV{HTTP_USER_AGENT} : '' ); if ( !$HttpUserAgent ) { $Self->{Browser} = 'Unknown - no $ENV{"HTTP_USER_AGENT"}'; } elsif ($HttpUserAgent) { # check, if we are on a mobile platform. # tablets are handled like desktops # only phones are "mobile" if ( $HttpUserAgent =~ /mobile/ ) { $Self->{IsMobile} = 1; } # android if ( $HttpUserAgent =~ /android/ ) { $Self->{Platform} = 'Android'; } # edge / spartan if ( $HttpUserAgent =~ /edge/ ) { $Self->{Browser} = 'Edge'; } # msie elsif ( $HttpUserAgent =~ /msie\s([0-9.]+)/ || $HttpUserAgent =~ /internet\sexplorer\/([0-9.]+)/ ) { $Self->{Browser} = 'MSIE'; if ( $1 =~ /(\d+)\.(\d+)/ ) { $Self->{BrowserMajorVersion} = $1; $Self->{BrowserMinorVersion} = $2; } # older windows mobile phones (until IE9), that still have 'MSIE' in the user agent string if ( $Self->{IsMobile} ) { $Self->{Platform} = 'Windows Phone'; } } # mobile ie elsif ( $HttpUserAgent =~ /iemobile/ ) { $Self->{Browser} = 'MSIE'; $Self->{Platform} = 'Windows Phone'; } # mobile ie (second try) elsif ( $HttpUserAgent =~ /trident/ ) { $Self->{Browser} = 'MSIE'; if ( $HttpUserAgent =~ /rv:([0-9])+\.([0-9])+/ ) { $Self->{BrowserMajorVersion} = $2; $Self->{BrowserMinorVersion} = $3; } } # iOS elsif ( $HttpUserAgent =~ /(ipad|iphone|ipod)/ ) { $Self->{Platform} = 'iOS'; $Self->{Browser} = 'Safari'; if ( $HttpUserAgent =~ /(ipad|iphone|ipod);.*cpu.*os ([0-9]+)_/ ) { $Self->{BrowserVersion} = $2; } if ( $HttpUserAgent =~ /crios/ ) { $Self->{Browser} = 'Chrome'; } # RichText is supported in iOS6+. if ( $Self->{BrowserVersion} >= 6 ) { $Self->{BrowserRichText} = 1; } else { $Self->{BrowserRichText} = 0; } } # safari elsif ( $HttpUserAgent =~ /safari/ ) { # chrome if ( $HttpUserAgent =~ /chrome/ ) { $Self->{Browser} = 'Chrome'; } else { $Self->{Browser} = 'Safari'; } } # konqueror elsif ( $HttpUserAgent =~ /konqueror/ ) { $Self->{Browser} = 'Konqueror'; # on konquerer disable rich text editor $Self->{BrowserRichText} = 0; } # firefox elsif ( $HttpUserAgent =~ /firefox/ ) { $Self->{Browser} = 'Firefox'; } # opera elsif ( $HttpUserAgent =~ /^opera.*/ ) { $Self->{Browser} = 'Opera'; } # netscape elsif ( $HttpUserAgent =~ /netscape/ ) { $Self->{Browser} = 'Netscape'; } # w3m elsif ( $HttpUserAgent =~ /^w3m.*/ ) { $Self->{Browser} = 'w3m'; $Self->{BrowserJavaScriptSupport} = 0; } # lynx elsif ( $HttpUserAgent =~ /^lynx.*/ ) { $Self->{Browser} = 'Lynx'; $Self->{BrowserJavaScriptSupport} = 0; } # links elsif ( $HttpUserAgent =~ /^links.*/ ) { $Self->{Browser} = 'Links'; } else { $Self->{Browser} = 'Unknown - ' . $HttpUserAgent; } } # check mobile devices to disable richtext support if ( $Self->{IsMobile} && $Self->{Platform} ne 'iOS' && $Self->{Platform} ne 'Android' && $Self->{Platform} ne 'Windows Phone' ) { $Self->{BrowserRichText} = 0; } # check if rich text can be active if ( !$Self->{BrowserJavaScriptSupport} || !$Self->{BrowserRichText} ) { $ConfigObject->Set( Key => 'Frontend::RichText', Value => 0, ); } # check if rich text is active if ( !$ConfigObject->Get('Frontend::RichText') ) { $Self->{BrowserRichText} = 0; } # load theme my $Theme = $Self->{UserTheme} || $ConfigObject->Get('DefaultTheme') || Translatable('Standard'); # force a theme based on host name my $DefaultThemeHostBased = $ConfigObject->Get('DefaultTheme::HostBased'); if ( $DefaultThemeHostBased && $ENV{HTTP_HOST} ) { THEME: for my $RegExp ( sort keys %{$DefaultThemeHostBased} ) { # do not use empty regexp or theme directories next THEME if !$RegExp; next THEME if $RegExp eq ''; next THEME if !$DefaultThemeHostBased->{$RegExp}; # check if regexp is matching if ( $ENV{HTTP_HOST} =~ /$RegExp/i ) { $Theme = $DefaultThemeHostBased->{$RegExp}; } } } # locate template files $Self->{TemplateDir} = $ConfigObject->Get('TemplateDir') . '/HTML/Templates/' . $Theme; $Self->{StandardTemplateDir} = $ConfigObject->Get('TemplateDir') . '/HTML/Templates/' . 'Standard'; # Check if 'Standard' fallback exists if ( !-e $Self->{StandardTemplateDir} ) { $Self->FatalDie( Message => "No existing template directory found ('$Self->{TemplateDir}')! Check your Home in Kernel/Config.pm." ); } if ( !-e $Self->{TemplateDir} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "No existing template directory found ('$Self->{TemplateDir}')!. Default theme used instead.", ); # Set TemplateDir to 'Standard' as a fallback. $Theme = 'Standard'; $Self->{TemplateDir} = $Self->{StandardTemplateDir}; } $Self->{CustomTemplateDir} = $ConfigObject->Get('CustomTemplateDir') . '/HTML/Templates/' . $Theme; $Self->{CustomStandardTemplateDir} = $ConfigObject->Get('CustomTemplateDir') . '/HTML/Templates/' . 'Standard'; # get main object my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); # load sub layout files my $NewDir = $ConfigObject->Get('TemplateDir') . '/HTML/Layout'; if ( -e $NewDir ) { my @NewFiles = $MainObject->DirectoryRead( Directory => $NewDir, Filter => '*.pm', ); for my $NewFile (@NewFiles) { if ( $NewFile !~ /Layout.pm$/ ) { $NewFile =~ s{\A.*\/(.+?).pm\z}{$1}xms; my $NewClassName = "Kernel::Output::HTML::Layout::$NewFile"; if ( !$MainObject->RequireBaseClass($NewClassName) ) { $Self->FatalDie( Message => "Could not load class Kernel::Output::HTML::Layout::$NewFile.", ); } } } } if ( $Self->{SessionID} && $Self->{UserChallengeToken} ) { $Self->{ChallengeTokenParam} = "ChallengeToken=$Self->{UserChallengeToken};"; } # load NavigationModule if defined if ( $Self->{ModuleReg} ) { my $NavigationModule = $Kernel::OM->Get('Kernel::Config')->Get("Frontend::NavigationModule"); if ( $NavigationModule->{ $Param{Action} } ) { $Self->{NavigationModule} = $NavigationModule->{ $Param{Action} }; } } return $Self; } sub SetEnv { my ( $Self, %Param ) = @_; for (qw(Key Value)) { if ( !defined $Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); $Self->FatalError(); } } $Self->{EnvNewRef}->{ $Param{Key} } = $Param{Value}; return 1; } =head2 Block() call a block and pass data to it (optional) to generate the block's output. $LayoutObject->Block( Name => 'Row', Data => { Time => ..., }, ); =cut sub Block { my ( $Self, %Param ) = @_; if ( !$Param{Name} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Name!' ); return; } push @{ $Self->{BlockData} }, { Name => $Param{Name}, Data => $Param{Data}, }; return 1; } =head2 JSONEncode() Encode perl data structure to JSON string my $JSON = $LayoutObject->JSONEncode( Data => $Data, NoQuotes => 0|1, # optional: no double quotes at the start and the end of JSON string ); =cut sub JSONEncode { my ( $Self, %Param ) = @_; # check for needed data return if !defined $Param{Data}; # get JSON encoded data my $JSON = $Kernel::OM->Get('Kernel::System::JSON')->Encode( Data => $Param{Data}, ) || '""'; # remove trailing and trailing double quotes if requested if ( $Param{NoQuotes} ) { $JSON =~ s{ \A "(.*)" \z }{$1}smx; } return $JSON; } =head2 Redirect() return html for browser to redirect my $HTML = $LayoutObject->Redirect( OP => "Action=AdminUserGroup;Subaction=User;ID=$UserID", ); my $HTML = $LayoutObject->Redirect( ExtURL => "http://some.example.com/", ); During login action, C 1> should be passed to Redirect(), which indicates that if the browser has cookie support, it is OK for the session cookie to be not yet set. =cut sub Redirect { my ( $Self, %Param ) = @_; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # add cookies if exists my $Cookies = ''; if ( $Self->{SetCookies} && $ConfigObject->Get('SessionUseCookie') ) { for ( sort keys %{ $Self->{SetCookies} } ) { $Cookies .= "Set-Cookie: $Self->{SetCookies}->{$_}\n"; } } # create & return output if ( $Param{ExtURL} ) { # external redirect $Param{Redirect} = $Param{ExtURL}; return $Cookies . $Self->Output( TemplateFile => 'Redirect', Data => \%Param ); } # set baselink $Param{Redirect} = $Self->{Baselink}; if ( $Param{OP} ) { # Filter out hazardous characters if ( $Param{OP} =~ s{\x00}{}smxg ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Someone tries to use a null bytes (\x00) character in redirect!', ); } if ( $Param{OP} =~ s{\r}{}smxg ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Someone tries to use a carriage return character in redirect!', ); } if ( $Param{OP} =~ s{\n}{}smxg ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Someone tries to use a newline character in redirect!', ); } # internal redirect $Param{OP} =~ s/^.*\?(.+?)$/$1/; $Param{Redirect} .= $Param{OP}; } # check if IIS 6 is used, add absolute url for IIS workaround # see also: # o http://bugs.otrs.org/show_bug.cgi?id=2230 # o http://bugs.otrs.org/show_bug.cgi?id=9835 # o http://support.microsoft.com/default.aspx?scid=kb;en-us;221154 if ( $ENV{SERVER_SOFTWARE} =~ /^microsoft\-iis\/6/i ) { my $Host = $ENV{HTTP_HOST} || $ConfigObject->Get('FQDN'); my $HttpType = $ConfigObject->Get('HttpType'); $Param{Redirect} = $HttpType . '://' . $Host . $Param{Redirect}; } my $Output = $Cookies . $Self->Output( TemplateFile => 'Redirect', Data => \%Param ); # add session id to redirect if no cookie is enabled if ( !$Self->{SessionIDCookie} && !( $Self->{BrowserHasCookie} && $Param{Login} ) ) { # rewrite location header $Output =~ s{ (location:\s)(.*) } { my $Start = $1; my $Target = $2; my $End = ''; if ($Target =~ /^(.+?)#(|.+?)$/) { $Target = $1; $End = "#$2"; } if ($Target =~ /http/i || !$Self->{SessionID}) { "$Start$Target$End"; } else { if ($Target =~ /(\?|&)$/) { "$Start$Target$Self->{SessionName}=$Self->{SessionID}$End"; } elsif ($Target !~ /\?/) { "$Start$Target?$Self->{SessionName}=$Self->{SessionID}$End"; } elsif ($Target =~ /\?/) { "$Start$Target&$Self->{SessionName}=$Self->{SessionID}$End"; } else { "$Start$Target?&$Self->{SessionName}=$Self->{SessionID}$End"; } } }iegx; } return $Output; } sub Login { my ( $Self, %Param ) = @_; # set Action parameter for the loader $Self->{Action} = 'Login'; $Param{IsLoginPage} = 1; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $Output = ''; if ( $ConfigObject->Get('SessionUseCookie') ) { # always set a cookie, so that at the time the user submits # the password, we know already if the browser supports cookies. # ( the session cookie isn't available at that time ). my $CookieSecureAttribute = 0; if ( $ConfigObject->Get('HttpType') eq 'https' ) { # Restrict Cookie to HTTPS if it is used. $CookieSecureAttribute = 1; } $Self->{SetCookies}->{OTRSBrowserHasCookie} = $Kernel::OM->Get('Kernel::System::Web::Request')->SetCookie( Key => 'OTRSBrowserHasCookie', Value => 1, Expires => '+1y', Path => $ConfigObject->Get('ScriptAlias'), Secure => $CookieSecureAttribute, HttpOnly => 1, ); } # add cookies if exists if ( $Self->{SetCookies} && $ConfigObject->Get('SessionUseCookie') ) { for ( sort keys %{ $Self->{SetCookies} } ) { $Output .= "Set-Cookie: $Self->{SetCookies}->{$_}\n"; } } # get message of the day if ( $ConfigObject->Get('ShowMotd') ) { $Param{Motd} = $Self->Output( TemplateFile => 'Motd', Data => \%Param ); } # Generate the minified CSS and JavaScript files and the tags referencing them (see LayoutLoader) $Self->LoaderCreateAgentCSSCalls(); $Self->LoaderCreateAgentJSCalls(); $Self->LoaderCreateJavaScriptTranslationData(); $Self->LoaderCreateJavaScriptTemplateData(); my $OTRSBusinessObject = $Kernel::OM->Get('Kernel::System::OTRSBusiness'); $Param{OTRSBusinessIsInstalled} = $OTRSBusinessObject->OTRSBusinessIsInstalled(); $Param{OTRSSTORMIsInstalled} = $OTRSBusinessObject->OTRSSTORMIsInstalled(); $Param{OTRSCONTROLIsInstalled} = $OTRSBusinessObject->OTRSCONTROLIsInstalled(); # we need the baselink for VerfifiedGet() of selenium tests $Self->AddJSData( Key => 'Baselink', Value => $Self->{Baselink}, ); # Add header logo, if configured if ( defined $ConfigObject->Get('AgentLogo') ) { my %AgentLogo = %{ $ConfigObject->Get('AgentLogo') }; my %Data; for my $CSSStatement ( sort keys %AgentLogo ) { if ( $CSSStatement eq 'URL' ) { my $WebPath = ''; if ( $AgentLogo{$CSSStatement} !~ /(http|ftp|https):\//i ) { $WebPath = $ConfigObject->Get('Frontend::WebPath'); } $Data{'URL'} = 'url(' . $WebPath . $AgentLogo{$CSSStatement} . ')'; } else { $Data{$CSSStatement} = $AgentLogo{$CSSStatement}; } } $Self->Block( Name => 'HeaderLogoCSS', Data => \%Data, ); } # add login logo, if configured if ( defined $ConfigObject->Get('AgentLoginLogo') ) { my %AgentLoginLogo = %{ $ConfigObject->Get('AgentLoginLogo') }; my %Data; for my $CSSStatement ( sort keys %AgentLoginLogo ) { if ( $CSSStatement eq 'URL' ) { my $WebPath = ''; if ( $AgentLoginLogo{$CSSStatement} !~ /(http|ftp|https):\//i ) { $WebPath = $ConfigObject->Get('Frontend::WebPath'); } $Data{'URL'} = 'url(' . $WebPath . $AgentLoginLogo{$CSSStatement} . ')'; } else { $Data{$CSSStatement} = $AgentLoginLogo{$CSSStatement}; } } $Self->Block( Name => 'LoginLogoCSS', Data => \%Data, ); $Self->Block( Name => 'LoginLogo' ); } # get system maintenance object my $SystemMaintenanceObject = $Kernel::OM->Get('Kernel::System::SystemMaintenance'); my $ActiveMaintenance = $SystemMaintenanceObject->SystemMaintenanceIsActive(); # check if system maintenance is active if ($ActiveMaintenance) { my $SystemMaintenanceData = $SystemMaintenanceObject->SystemMaintenanceGet( ID => $ActiveMaintenance, UserID => 1, ); if ( $SystemMaintenanceData->{ShowLoginMessage} ) { my $LoginMessage = $SystemMaintenanceData->{LoginMessage} || $ConfigObject->Get('SystemMaintenance::IsActiveDefaultLoginMessage') || "System maintenance is active, not possible to perform a login!"; $Self->Block( Name => 'SystemMaintenance', Data => { LoginMessage => $LoginMessage, }, ); } } # show prelogin block, if in prelogin mode (e.g. SSO login) if ( defined $Param{'Mode'} && $Param{'Mode'} eq 'PreLogin' ) { $Self->Block( Name => 'PreLogin', Data => \%Param, ); } # if not in PreLogin mode, show normal login form else { $Self->Block( Name => 'LoginBox', Data => \%Param, ); # show 2 factor password input if we have at least one backend enabled COUNT: for my $Count ( '', 1 .. 10 ) { next COUNT if !$ConfigObject->Get("AuthTwoFactorModule$Count"); # if no empty shared secrets are allowed, input is mandatory my %MandatoryOptions; if ( !$ConfigObject->Get("AuthTwoFactorModule${Count}::AllowEmptySecret") ) { %MandatoryOptions = ( MandatoryClass => 'Mandatory', ValidateRequired => 'Validate_Required', ); } $Self->Block( Name => 'AuthTwoFactor', Data => { %Param, %MandatoryOptions, }, ); if (%MandatoryOptions) { $Self->Block( Name => 'AuthTwoFactorMandatory', Data => \%Param, ); } last COUNT; } # get lost password if ( $ConfigObject->Get('LostPassword') && $ConfigObject->Get('AuthModule') eq 'Kernel::System::Auth::DB' ) { $Self->Block( Name => 'LostPasswordLink', Data => \%Param, ); $Self->Block( Name => 'LostPassword', Data => \%Param, ); } } # send data to JS $Self->AddJSData( Key => 'LoginFailed', Value => $Param{LoginFailed}, ); # create & return output $Output .= $Self->Output( TemplateFile => 'Login', Data => \%Param, ); # remove the version tag from the header if configured $Self->_DisableBannerCheck( OutputRef => \$Output ); return $Output; } sub ChallengeTokenCheck { my ( $Self, %Param ) = @_; # return if feature is disabled return 1 if !$Kernel::OM->Get('Kernel::Config')->Get('SessionCSRFProtection'); # get challenge token and check it my $ChallengeToken = $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => 'ChallengeToken' ) || ''; # check regular ChallengeToken return 1 if $ChallengeToken eq $Self->{UserChallengeToken}; # check ChallengeToken of all own sessions my $SessionObject = $Kernel::OM->Get('Kernel::System::AuthSession'); my @Sessions = $SessionObject->GetAllSessionIDs(); SESSION: for my $SessionID (@Sessions) { my %Data = $SessionObject->GetSessionIDData( SessionID => $SessionID ); next SESSION if !$Data{UserID}; next SESSION if $Data{UserID} ne $Self->{UserID}; next SESSION if !$Data{UserChallengeToken}; # check ChallengeToken return 1 if $ChallengeToken eq $Data{UserChallengeToken}; } # no valid token found if ( $Param{Type} && lc $Param{Type} eq 'customer' ) { $Self->CustomerFatalError( Message => 'Invalid Challenge Token!', ); } else { $Self->FatalError( Message => 'Invalid Challenge Token!', ); } return; } sub FatalError { my ( $Self, %Param ) = @_; # Prevent endless recursion in case of problems with Template engine. return if ( $Self->{InFatalError}++ ); if ( $Param{Message} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Caller => 1, Priority => 'error', Message => $Param{Message}, ); } my $Output = $Self->Header( Area => 'Frontend', Title => 'Fatal Error' ); $Output .= $Self->Error(%Param); $Output .= $Self->Footer(); $Self->Print( Output => \$Output ); exit; } sub SecureMode { my ( $Self, %Param ) = @_; my $Output = $Self->Header( Area => 'Frontend', Title => 'Secure Mode' ); $Output .= $Self->Output( TemplateFile => 'AdminSecureMode', Data => \%Param ); $Output .= $Self->Footer(); return $Output; } sub FatalDie { my ( $Self, %Param ) = @_; if ( $Param{Message} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Caller => 1, Priority => 'error', Message => $Param{Message}, ); } # get backend error messages for (qw(Message Traceback)) { my $Backend = 'Backend' . $_; $Param{$Backend} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => $_ ) || ''; $Param{$Backend} = $Self->Ascii2Html( Text => $Param{$Backend}, HTMLResultMode => 1, ); } if ( !$Param{Message} ) { $Param{Message} = $Param{BackendMessage}; } die $Param{Message}; } sub ErrorScreen { my ( $Self, %Param ) = @_; my $Output = $Self->Header( Title => 'Error' ); $Output .= $Self->Error(%Param); $Output .= $Self->Footer(); return $Output; } sub Error { my ( $Self, %Param ) = @_; # get backend error messages for (qw(Message Traceback)) { my $Backend = 'Backend' . $_; $Param{$Backend} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => $_ ) || ''; } if ( !$Param{BackendMessage} && !$Param{BackendTraceback} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Param{Message} || '?', ); for (qw(Message Traceback)) { my $Backend = 'Backend' . $_; $Param{$Backend} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => $_ ) || ''; } } if ( !$Param{Message} ) { $Param{Message} = $Param{BackendMessage}; # Don't check for business package if the database was not yet configured (in the installer). if ( $Kernel::OM->Get('Kernel::Config')->Get('SecureMode') && $Kernel::OM->Get('Kernel::Config')->Get('DatabaseDSN') && !$Kernel::OM->Get('Kernel::System::OTRSBusiness')->OTRSBusinessIsInstalled() ) { $Param{ShowOTRSBusinessHint}++; } } if ( $Param{BackendTraceback} ) { $Self->Block( Name => 'ShowBackendTraceback', Data => \%Param, ); } # create & return output return $Self->Output( TemplateFile => 'Error', Data => \%Param ); } sub Warning { my ( $Self, %Param ) = @_; # get backend error messages $Param{BackendMessage} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Notice', What => 'Message', ) || $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => 'Message', ) || ''; if ( !$Param{Message} ) { $Param{Message} = $Param{BackendMessage}; } # create & return output return $Self->Output( TemplateFile => 'Warning', Data => \%Param ); } =head2 Notify() create notify lines infos, the text will be translated my $Output = $LayoutObject->Notify( Priority => 'Warning', Info => 'Some Info Message', ); data with link, the text will be translated my $Output = $LayoutObject->Notify( Priority => 'Warning', Data => 'Template content', Link => 'http://example.com/', LinkClass => 'some_CSS_class', # optional ); errors, the text will be translated my $Output = $LayoutObject->Notify( Priority => 'Error', Info => 'Some Error Message', ); errors from log backend, if no error exists, a '' will be returned my $Output = $LayoutObject->Notify( Priority => 'Error', ); =cut sub Notify { my ( $Self, %Param ) = @_; # create & return output if ( !$Param{Info} && !$Param{Data} ) { $Param{BackendMessage} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Notice', What => 'Message', ) || $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => 'Message', ) || ''; $Param{Info} = $Param{BackendMessage}; # return if we have nothing to show return '' if !$Param{Info}; } my $BoxClass = 'Notice'; if ( $Param{Info} ) { $Param{Info} =~ s/\n//g; } if ( $Param{Priority} && $Param{Priority} eq 'Error' ) { $BoxClass = 'Error'; } elsif ( $Param{Priority} && $Param{Priority} eq 'Success' ) { $BoxClass = 'Success'; } elsif ( $Param{Priority} && $Param{Priority} eq 'Info' ) { $BoxClass = 'Info'; } if ( $Param{Link} ) { $Self->Block( Name => 'LinkStart', Data => { LinkStart => $Param{Link}, LinkClass => $Param{LinkClass} || '', }, ); } if ( $Param{Data} ) { $Self->Block( Name => 'Data', Data => \%Param, ); } else { $Self->Block( Name => 'Text', Data => \%Param, ); } if ( $Param{Link} ) { $Self->Block( Name => 'LinkStop', Data => { LinkStop => '', }, ); } return $Self->Output( TemplateFile => 'Notify', Data => { %Param, BoxClass => $BoxClass, }, ); } =head2 NotifyNonUpdatedTickets() Adds notification about tickets which are not updated. my $Output = $LayoutObject->NotifyNonUpdatedTickets(); =cut sub NotifyNonUpdatedTickets { my ( $Self, %Param ) = @_; my $NonUpdatedTicketsString = $Kernel::OM->Get('Kernel::System::Cache')->Get( Type => 'Ticket', Key => 'NonUpdatedTicketsString-' . $Self->{UserID}, ); return if !$NonUpdatedTicketsString; # Delete this value from the cache. $Kernel::OM->Get('Kernel::System::Cache')->Delete( Type => 'Ticket', Key => 'NonUpdatedTicketsString-' . $Self->{UserID}, ); return $Self->Notify( Info => $Self->{LanguageObject} ->Translate( "The following tickets are not updated: %s.", $NonUpdatedTicketsString ), ); } =head2 Header() generates the HTML for the page begin in the Agent interface. my $Output = $LayoutObject->Header( Type => 'Small', # (optional) '' (Default, full header) or 'Small' (blank header) ShowToolbarItems => 0, # (optional) default 1 (0|1) ShowPrefLink => 0, # (optional) default 1 (0|1) ShowLogoutButton => 0, # (optional) default 1 (0|1) DisableIFrameOriginRestricted => 1, # (optional, default 0) - suppress X-Frame-Options header. ); =cut sub Header { my ( $Self, %Param ) = @_; my $Type = $Param{Type} || ''; # check params if ( !defined $Param{ShowToolbarItems} ) { $Param{ShowToolbarItems} = 1; } if ( !defined $Param{ShowPrefLink} ) { $Param{ShowPrefLink} = 1; } my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # do not show preferences link if the preferences module is disabled my $Modules = $ConfigObject->Get('Frontend::Module'); if ( !$Modules->{AgentPreferences} ) { $Param{ShowPrefLink} = 0; } if ( !defined $Param{ShowLogoutButton} ) { $Param{ShowLogoutButton} = 1; } # set rtl if needed if ( $Self->{TextDirection} && $Self->{TextDirection} eq 'rtl' ) { $Param{BodyClass} = 'RTL'; } elsif ( $ConfigObject->Get('Frontend::DebugMode') ) { $Self->Block( Name => 'DebugRTLButton', ); } # Generate the minified CSS and JavaScript files and the tags referencing them (see LayoutLoader) $Self->LoaderCreateAgentCSSCalls(); my %AgentLogo; # check if we need to display a custom logo for the selected skin my $AgentLogoCustom = $ConfigObject->Get('AgentLogoCustom'); if ( $Self->{SkinSelected} && $AgentLogoCustom && IsHashRefWithData($AgentLogoCustom) && $AgentLogoCustom->{ $Self->{SkinSelected} } ) { %AgentLogo = %{ $AgentLogoCustom->{ $Self->{SkinSelected} } }; } # Otherwise show default header logo, if configured elsif ( defined $ConfigObject->Get('AgentLogo') ) { %AgentLogo = %{ $ConfigObject->Get('AgentLogo') }; } if ( %AgentLogo && keys %AgentLogo ) { my %Data; for my $CSSStatement ( sort keys %AgentLogo ) { if ( $CSSStatement eq 'URL' ) { my $WebPath = ''; if ( $AgentLogo{$CSSStatement} !~ /(http|ftp|https):\//i ) { $WebPath = $ConfigObject->Get('Frontend::WebPath'); } $Data{'URL'} = 'url(' . $WebPath . $AgentLogo{$CSSStatement} . ')'; } else { $Data{$CSSStatement} = $AgentLogo{$CSSStatement}; } } $Self->Block( Name => 'HeaderLogoCSS', Data => \%Data, ); } # add cookies if exists my $Output = ''; if ( $Self->{SetCookies} && $ConfigObject->Get('SessionUseCookie') ) { for ( sort keys %{ $Self->{SetCookies} } ) { $Output .= "Set-Cookie: $Self->{SetCookies}->{$_}\n"; } } my $File = $Param{Filename} || $Self->{Action} || 'unknown'; # set file name for "save page as" $Param{ContentDisposition} = "filename=\"$File.html\""; # area and title if ( !$Param{Area} ) { $Param{Area} = ( defined $Self->{Action} ? $ConfigObject->Get('Frontend::Module')->{ $Self->{Action} }->{NavBarName} : '' ); } if ( !$Param{Title} ) { $Param{Title} = $ConfigObject->Get('Frontend::Module')->{ $Self->{Action} }->{Title} || ''; } for my $Word (qw(Value Title Area)) { if ( $Param{$Word} ) { $Param{TitleArea} .= $Self->{LanguageObject}->Translate( $Param{$Word} ) . ' - '; } } my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); # run header meta modules my $HeaderMetaModule = $ConfigObject->Get('Frontend::HeaderMetaModule'); if ( ref $HeaderMetaModule eq 'HASH' ) { my %Jobs = %{$HeaderMetaModule}; MODULE: for my $Job ( sort keys %Jobs ) { # load and run module next MODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, ); next MODULE if !$Object; $Object->Run( %Param, Config => $Jobs{$Job} ); } } # run tool bar item modules if ( $Self->{UserID} && $Self->{UserType} eq 'User' ) { my $ToolBarModule = $ConfigObject->Get('Frontend::ToolBarModule'); if ( $Param{ShowToolbarItems} && ref $ToolBarModule eq 'HASH' ) { $Self->Block( Name => 'ToolBar', Data => \%Param, ); my %Modules; my %Jobs = %{$ToolBarModule}; # get group object my $GroupObject = $Kernel::OM->Get('Kernel::System::Group'); MODULE: for my $Job ( sort keys %Jobs ) { # load and run module next MODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, # UserID etc. ); next MODULE if !$Object; my $ToolBarAccessOk; # if group restriction for tool-bar is set, check user permission if ( $Jobs{$Job}->{Group} ) { # remove white-spaces $Jobs{$Job}->{Group} =~ s{\s}{}xmsg; # get group configurations my @Items = split( ';', $Jobs{$Job}->{Group} ); ITEM: for my $Item (@Items) { # split values into permission and group my ( $Permission, $GroupName ) = split( ':', $Item ); # log an error if not valid setting if ( !$Permission || !$GroupName ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Invalid config for ToolBarModule $Job - Key Group: '$Item'! " . "Need something like 'Permission:Group;'", ); } # get groups for current user my %Groups = $GroupObject->PermissionUserGet( UserID => $Self->{UserID}, Type => $Permission, ); # next job if user have not groups next ITEM if !%Groups; # check user belongs to the correct group my %GroupsReverse = reverse %Groups; next ITEM if !$GroupsReverse{$GroupName}; $ToolBarAccessOk = 1; last ITEM; } # go to the next module if not permissions # for the current one next MODULE if !$ToolBarAccessOk; } %Modules = ( $Object->Run( %Param, Config => $Jobs{$Job} ), %Modules ); } # show tool bar items MODULE: for my $Key ( sort keys %Modules ) { next MODULE if !%{ $Modules{$Key} }; # For ToolBarSearchFulltext module take into consideration SearchInArchive settings. # See bug#13790 (https://bugs.otrs.org/show_bug.cgi?id=13790). if ( $ConfigObject->Get('Ticket::ArchiveSystem') && $Modules{$Key}->{Block} eq 'ToolBarSearchFulltext' ) { $Modules{$Key}->{SearchInArchive} = $ConfigObject->Get('Ticket::Frontend::AgentTicketSearch')->{Defaults}->{SearchInArchive}; } $Self->Block( Name => $Modules{$Key}->{Block}, Data => { %{ $Modules{$Key} }, AccessKeyReference => $Modules{$Key}->{AccessKey} ? " ($Modules{$Key}->{AccessKey})" : '', }, ); } } if ( $Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::Chat', Silent => 1 ) ) { if ( $ConfigObject->Get('ChatEngine::Active') ) { $Self->AddJSData( Key => 'ChatEngine::Active', Value => $ConfigObject->Get('ChatEngine::Active') ); } } # generate avatar if ( $ConfigObject->Get('Frontend::AvatarEngine') eq 'Gravatar' && $Self->{UserEmail} ) { my $DefaultIcon = $ConfigObject->Get('Frontend::Gravatar::DefaultImage') || 'mm'; $Param{Avatar} = '//www.gravatar.com/avatar/' . md5_hex( lc $Self->{UserEmail} ) . '?s=100&d=' . $DefaultIcon; } else { my %User = $Kernel::OM->Get('Kernel::System::User')->GetUserData( User => $Self->{UserLogin}, NoOutOfOffice => 1, ); $Param{UserInitials} = $Self->UserInitialsGet( Fullname => $User{UserFullname} ); } # show logged in notice if ( $Param{ShowPrefLink} ) { $Self->Block( Name => 'Login', Data => \%Param, ); } else { $Self->Block( Name => 'LoginWithoutLink', Data => \%Param, ); } # show logout button (if registered) if ( $Param{ShowLogoutButton} && $ConfigObject->Get('Frontend::Module')->{Logout} ) { $Self->Block( Name => 'Logout', Data => \%Param, ); } } if ( $ConfigObject->Get('SecureMode') ) { $Param{OTRSBusinessIsInstalled} = $Kernel::OM->Get('Kernel::System::OTRSBusiness')->OTRSBusinessIsInstalled(); } # create & return output $Output .= $Self->Output( TemplateFile => "Header$Type", Data => \%Param ); # remove the version tag from the header if configured $Self->_DisableBannerCheck( OutputRef => \$Output ); return $Output; } sub Footer { my ( $Self, %Param ) = @_; my $Type = $Param{Type} || ''; my $HasDatepicker = $Self->{HasDatepicker} || 0; # generate the minified CSS and JavaScript files and the tags referencing them (see LayoutLoader) $Self->LoaderCreateAgentJSCalls(); $Self->LoaderCreateJavaScriptTranslationData(); $Self->LoaderCreateJavaScriptTemplateData(); # get datepicker data, if needed in module if ($HasDatepicker) { my $VacationDays = $Self->DatepickerGetVacationDays(); my $TextDirection = $Self->{LanguageObject}->{TextDirection} || ''; # send data to JS $Self->AddJSData( Key => 'Datepicker', Value => { VacationDays => $VacationDays, IsRTL => ( $TextDirection eq 'rtl' ) ? 1 : 0, }, ); } # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # send data to JS if NewTicketInNewWindow is enabled if ( $ConfigObject->Get('NewTicketInNewWindow::Enabled') ) { $Self->AddJSData( Key => 'NewTicketInNewWindow', Value => 1, ); } # AutoComplete-Config my $AutocompleteConfig = $ConfigObject->Get('AutoComplete::Agent'); for my $ConfigElement ( sort keys %{$AutocompleteConfig} ) { $AutocompleteConfig->{$ConfigElement}->{ButtonText} = $Self->{LanguageObject}->Translate( $AutocompleteConfig->{$ConfigElement}->{ButtonText} ); } # Search frontend (JavaScript) my $SearchFrontendConfig = $ConfigObject->Get('Frontend::Search::JavaScript'); # get target javascript function my $JSCall = ''; if ( $SearchFrontendConfig && $Self->{Action} ) { for my $Group ( sort keys %{$SearchFrontendConfig} ) { REGEXP: for my $RegExp ( sort keys %{ $SearchFrontendConfig->{$Group} } ) { if ( $Self->{Action} =~ /$RegExp/ ) { $JSCall = $SearchFrontendConfig->{$Group}->{$RegExp}; last REGEXP; } } } } # get OTRS business object my $OTRSBusinessObject = $Kernel::OM->Get('Kernel::System::OTRSBusiness'); # don't check for business package if the database was not yet configured (in the installer) if ( $ConfigObject->Get('SecureMode') ) { $Param{OTRSBusinessIsInstalled} = $OTRSBusinessObject->OTRSBusinessIsInstalled(); $Param{OTRSSTORMIsInstalled} = $OTRSBusinessObject->OTRSSTORMIsInstalled(); $Param{OTRSCONTROLIsInstalled} = $OTRSBusinessObject->OTRSCONTROLIsInstalled(); } # Check if video chat is enabled. if ( $Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::VideoChat', Silent => 1 ) ) { $Param{VideoChatEnabled} = $Kernel::OM->Get('Kernel::System::VideoChat')->IsEnabled() || $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => 'UnitTestMode' ) // 0; } # Set an array with pending states. my @PendingStateIDs = $Kernel::OM->Get('Kernel::System::State')->StateGetStatesByType( StateType => [ 'pending reminder', 'pending auto' ], Result => 'ID', ); # add JS data my %JSConfig = ( Baselink => $Self->{Baselink}, CGIHandle => $Self->{CGIHandle}, WebPath => $ConfigObject->Get('Frontend::WebPath'), Action => $Self->{Action}, Subaction => $Self->{Subaction}, SessionIDCookie => $Self->{SessionIDCookie}, SessionName => $Self->{SessionName}, SessionID => $Self->{SessionID}, SessionUseCookie => $ConfigObject->Get('SessionUseCookie'), ChallengeToken => $Self->{UserChallengeToken}, CustomerPanelSessionName => $ConfigObject->Get('CustomerPanelSessionName'), UserLanguage => $Self->{UserLanguage}, WebMaxFileUpload => $ConfigObject->Get('WebMaxFileUpload'), RichTextSet => $ConfigObject->Get('Frontend::RichText'), CheckEmailAddresses => $ConfigObject->Get('CheckEmailAddresses'), MenuDragDropEnabled => $ConfigObject->Get('Frontend::MenuDragDropEnabled'), OpenMainMenuOnHover => $ConfigObject->Get('OpenMainMenuOnHover'), CustomerInfoSet => $ConfigObject->Get('Ticket::Frontend::CustomerInfoCompose'), IncludeUnknownTicketCustomers => $ConfigObject->Get('Ticket::IncludeUnknownTicketCustomers'), InputFieldsActivated => $ConfigObject->Get('ModernizeFormFields'), OTRSBusinessIsInstalled => $Param{OTRSBusinessIsInstalled}, VideoChatEnabled => $Param{VideoChatEnabled}, PendingStateIDs => \@PendingStateIDs, CheckSearchStringsForStopWords => ( $ConfigObject->Get('Ticket::SearchIndex::WarnOnStopWordUsage') && ( $ConfigObject->Get('Ticket::SearchIndexModule') eq 'Kernel::System::Ticket::ArticleSearchIndex::DB' ) ) ? 1 : 0, SearchFrontend => $JSCall, Autocomplete => $AutocompleteConfig, ); for my $Config ( sort keys %JSConfig ) { $Self->AddJSData( Key => $Config, Value => $JSConfig{$Config}, ); } # create & return output return $Self->Output( TemplateFile => "Footer$Type", Data => \%Param ); } sub Print { my ( $Self, %Param ) = @_; # run output content filters if ( $Self->{FilterContent} && ref $Self->{FilterContent} eq 'HASH' ) { # extract filter list my %FilterList = %{ $Self->{FilterContent} }; my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); FILTER: for my $Filter ( sort keys %FilterList ) { # extract filter config my $FilterConfig = $FilterList{$Filter}; next FILTER if !$FilterConfig; next FILTER if ref $FilterConfig ne 'HASH'; # extract template list my $TemplateList = $FilterConfig->{Templates}; # check template list if ( !$TemplateList || ref $TemplateList ne 'HASH' || !%{$TemplateList} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Please add a template list to output filter $FilterConfig->{Module} " . "to improve performance. Use ALL if OutputFilter should modify all " . "templates of the system (deprecated).", ); } # check template list if ( $Param{TemplateFile} && ref $TemplateList eq 'HASH' && !$TemplateList->{ALL} ) { next FILTER if !$TemplateList->{ $Param{TemplateFile} }; } next FILTER if !$MainObject->Require( $FilterConfig->{Module} ); # create new instance my $Object = $FilterConfig->{Module}->new( %{$Self}, LayoutObject => $Self, ); next FILTER if !$Object; # run output filter $Object->Run( %{$FilterConfig}, Data => $Param{Output}, TemplateFile => $Param{TemplateFile} || '', ); } } # There seems to be a bug in FastCGI that it cannot handle unicode output properly. # Work around this by converting to an utf8 byte stream instead. # See also http://bugs.otrs.org/show_bug.cgi?id=6284 and # http://bugs.otrs.org/show_bug.cgi?id=9802. if ( $INC{'CGI/Fast.pm'} || $ENV{FCGI_ROLE} || $ENV{FCGI_SOCKET_PATH} ) { # are we on FCGI? $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( $Param{Output} ); binmode STDOUT, ':bytes'; } # Disable perl warnings in case of printing unicode private chars, # see https://rt.perl.org/Public/Bug/Display.html?id=121226. no warnings 'nonchar'; ## no critic print ${ $Param{Output} }; return 1; } =head2 Ascii2Html() convert ASCII to html string my $HTML = $LayoutObject->Ascii2Html( Text => 'Some <> Test Test', Max => 20, # max 20 chars flowed by [..] VMax => 15, # first 15 lines NewLine => 0, # move \r to \n HTMLResultMode => 0, # replace " " with C< > StripEmptyLines => 0, Type => 'Normal', # JSText or Normal text LinkFeature => 0, # do some URL detections ); also string ref is possible my $HTMLStringRef = $LayoutObject->Ascii2Html( Text => \$String, ); =cut sub Ascii2Html { my ( $Self, %Param ) = @_; # check needed param return '' if !defined $Param{Text}; # check text my $TextScalar; my $Text; if ( !ref $Param{Text} ) { $TextScalar = 1; $Text = \$Param{Text}; } elsif ( ref $Param{Text} eq 'SCALAR' ) { $Text = $Param{Text}; } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Invalid ref "' . ref( $Param{Text} ) . '" of Text param!', ); return ''; } # run output filter text my @Filters; if ( $Param{LinkFeature} && $Self->{FilterText} && ref $Self->{FilterText} eq 'HASH' ) { # extract filter list my %FilterList = %{ $Self->{FilterText} }; my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); FILTER: for my $Filter ( sort keys %FilterList ) { # extract filter config my $FilterConfig = $FilterList{$Filter}; next FILTER if !$FilterConfig; next FILTER if ref $FilterConfig ne 'HASH'; # extract template list my $TemplateList = $FilterConfig->{Templates}; # check template list if ( !$TemplateList || ref $TemplateList ne 'HASH' || !%{$TemplateList} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Please add a template list to output filter $FilterConfig->{Module} " . "to improve performance. Use ALL if OutputFilter should modify all " . "templates of the system (deprecated).", ); } # check template list if ( $Param{TemplateFile} && ref $TemplateList eq 'HASH' && !$TemplateList->{ALL} ) { next FILTER if !$TemplateList->{ $Param{TemplateFile} }; } $Self->FatalDie() if !$MainObject->Require( $FilterConfig->{Module} ); # create new instance my $Object = $FilterConfig->{Module}->new( %{$Self}, LayoutObject => $Self, ); next FILTER if !$Object; push( @Filters, { Object => $Object, Filter => $FilterConfig, }, ); } # pre run for my $Filter (@Filters) { $Text = $Filter->{Object}->Pre( Filter => $Filter->{Filter}, Data => $Text, ); } } # max width if ( $Param{Max} && length ${$Text} > $Param{Max} ) { ${$Text} = substr( ${$Text}, 0, $Param{Max} - 5 ) . '[...]'; } # newline if ( $Param{NewLine} && length( ${$Text} ) < 140_000 ) { ${$Text} =~ s/(\n\r|\r\r\n|\r\n)/\n/g; ${$Text} =~ s/\r/\n/g; ${$Text} =~ s/(.{4,$Param{NewLine}})(?:\s|\z)/$1\n/gm; } # remove tabs ${$Text} =~ s/\t/ /g; # strip empty lines if ( $Param{StripEmptyLines} ) { ${$Text} =~ s/^\s*\n//mg; } # max lines if ( $Param{VMax} ) { my @TextList = split( "\n", ${$Text} ); ${$Text} = ''; my $Counter = 1; for (@TextList) { if ( $Counter <= $Param{VMax} ) { ${$Text} .= $_ . "\n"; } $Counter++; } if ( $Counter >= $Param{VMax} ) { ${$Text} .= "[...]\n"; } } # html quoting ${$Text} =~ s/&/&/g; ${$Text} =~ s//>/g; ${$Text} =~ s/"/"/g; # text -> html format quoting if ( $Param{LinkFeature} ) { for my $Filter (@Filters) { $Text = $Filter->{Object}->Post( Filter => $Filter->{Filter}, Data => $Text, ); } } if ( $Param{HTMLResultMode} ) { ${$Text} =~ s/\n/\n/g; ${$Text} =~ s/ /  /g; # Convert the space at the beginning of the line (see bug#14346 - https://bugs.otrs.org/show_bug.cgi?id=14346). ${$Text} =~ s/\n /\n /g; } if ( $Param{Type} && $Param{Type} eq 'JSText' ) { ${$Text} =~ s/'/\\'/g; } return $Text if ref $Param{Text}; return ${$Text}; } =head2 LinkQuote() detect links in text my $HTMLWithLinks = $LayoutObject->LinkQuote( Text => $HTMLWithOutLinks, ); also string ref is possible my $HTMLWithLinksRef = $LayoutObject->LinkQuote( Text => \$HTMLWithOutLinksRef, ); =cut sub LinkQuote { my ( $Self, %Param ) = @_; my $Text = $Param{Text} || ''; my $Target = $Param{Target} || 'NewPage' . int( rand(199) ); # check ref my $TextScalar; if ( !ref $Text ) { $TextScalar = $Text; $Text = \$TextScalar; } # run output filter text my @Filters; if ( $Self->{FilterText} && ref $Self->{FilterText} eq 'HASH' ) { # extract filter list my %FilterList = %{ $Self->{FilterText} }; my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); FILTER: for my $Filter ( sort keys %FilterList ) { # extract filter config my $FilterConfig = $FilterList{$Filter}; next FILTER if !$FilterConfig; next FILTER if ref $FilterConfig ne 'HASH'; # extract template list my $TemplateList = $FilterConfig->{Templates}; # check template list if ( !$TemplateList || ref $TemplateList ne 'HASH' || !%{$TemplateList} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Please add a template list to output filter $FilterConfig->{Module} " . "to improve performance. Use ALL if OutputFilter should modify all " . "templates of the system (deprecated).", ); } # check template list if ( $Param{TemplateFile} && ref $TemplateList eq 'HASH' && !$TemplateList->{ALL} ) { next FILTER if !$TemplateList->{ $Param{TemplateFile} }; } $Self->FatalDie() if !$MainObject->Require( $FilterConfig->{Module} ); # create new instance my $Object = $FilterConfig->{Module}->new( %{$Self}, LayoutObject => $Self, ); next FILTER if !$Object; push @Filters, { Object => $Object, Filter => $FilterConfig, }; } } for my $Filter (@Filters) { $Text = $Filter->{Object}->Pre( Filter => $Filter->{Filter}, Data => $Text ); } for my $Filter (@Filters) { $Text = $Filter->{Object}->Post( Filter => $Filter->{Filter}, Data => $Text ); } # do mail to quote ${$Text} =~ s/(mailto:.+?)(\.\s|\s|\)|\"|]|')/$1<\/a>$2/gi; # check ref && return result like called if ($TextScalar) { return ${$Text}; } else { return $Text; } } =head2 HTMLLinkQuote() detect links in HTML code my $HTMLWithLinks = $LayoutObject->HTMLLinkQuote( String => $HTMLString, ); also string ref is possible my $HTMLWithLinksRef = $LayoutObject->HTMLLinkQuote( String => \$HTMLString, ); =cut sub HTMLLinkQuote { my ( $Self, %Param ) = @_; return $Kernel::OM->Get('Kernel::System::HTMLUtils')->LinkQuote( String => $Param{String}, TargetAdd => 1, Target => '_blank', ); } =head2 LinkEncode() perform URL encoding on query string parameter names or values. my $ParamValueEncoded = $LayoutObject->LinkEncode($ParamValue); Don't encode entire URLs, because this will make them invalid (?, & and ; will be encoded as well). Only pass one parameter name or value at a time. =cut sub LinkEncode { my ( $Self, $Link ) = @_; return if !defined $Link; return URI::Escape::uri_escape_utf8($Link); } sub CustomerAgeInHours { my ( $Self, %Param ) = @_; my $Age = defined( $Param{Age} ) ? $Param{Age} : return; my $Space = $Param{Space} || '
'; my $AgeStrg = ''; my $HourDsc = Translatable('h'); my $MinuteDsc = Translatable('m'); if ( $Kernel::OM->Get('Kernel::Config')->Get('TimeShowCompleteDescription') ) { $HourDsc = Translatable('hour(s)'); $MinuteDsc = Translatable('minute(s)'); } if ( $Age =~ /^-(.*)/ ) { $Age = $1; $AgeStrg = '-'; } # get hours if ( $Age >= 3600 ) { $AgeStrg .= int( ( $Age / 3600 ) ) . ' '; $AgeStrg .= $Self->{LanguageObject}->Translate($HourDsc); $AgeStrg .= $Space; } # get minutes (just if age < 1 day) if ( $Age <= 3600 || int( ( $Age / 60 ) % 60 ) ) { $AgeStrg .= int( ( $Age / 60 ) % 60 ) . ' '; $AgeStrg .= $Self->{LanguageObject}->Translate($MinuteDsc); } return $AgeStrg; } sub CustomerAge { my ( $Self, %Param ) = @_; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $Age = defined( $Param{Age} ) ? $Param{Age} : return; my $Space = $Param{Space} || '
'; my $AgeStrg = ''; my $DayDsc = Translatable('d'); my $HourDsc = Translatable('h'); my $MinuteDsc = Translatable('m'); if ( $ConfigObject->Get('TimeShowCompleteDescription') ) { $DayDsc = Translatable('day(s)'); $HourDsc = Translatable('hour(s)'); $MinuteDsc = Translatable('minute(s)'); } if ( $Age =~ /^-(.*)/ ) { $Age = $1; $AgeStrg = '-'; } # get days if ( $Age >= 86400 ) { $AgeStrg .= int( ( $Age / 3600 ) / 24 ) . ' '; $AgeStrg .= $Self->{LanguageObject}->Translate($DayDsc); $AgeStrg .= $Space; } # get hours if ( $Age >= 3600 ) { $AgeStrg .= int( ( $Age / 3600 ) % 24 ) . ' '; $AgeStrg .= $Self->{LanguageObject}->Translate($HourDsc); $AgeStrg .= $Space; } # get minutes (just if age < 1 day) if ( ( $Param{TimeShowAlwaysLong} || $ConfigObject->Get('TimeShowAlwaysLong') || $Age < 86400 ) && $Age != 0 ) { $AgeStrg .= int( ( $Age / 60 ) % 60 ) . ' '; $AgeStrg .= $Self->{LanguageObject}->Translate($MinuteDsc); } return $AgeStrg; } =head2 BuildSelection() build a HTML option element based on given data my $HTML = $LayoutObject->BuildSelection( Data => $ArrayRef, # use $HashRef, $ArrayRef or $ArrayHashRef (see below) Name => 'TheName', # name of element ID => 'HTMLID', # (optional) the HTML ID for this element, if not provided, the name will be used as ID as well Multiple => 0, # (optional) default 0 (0|1) Size => 1, # (optional) default 1 element size Class => 'class', # (optional) a css class, include 'Modernize' to activate InputFields Disabled => 0, # (optional) default 0 (0|1) disable the element AutoComplete => 'off', # (optional) OnChange => 'javascript', # (optional) OnClick => 'javascript', # (optional) SelectedID => 1, # (optional) use integer or arrayref (unable to use with ArrayHashRef) SelectedID => [1, 5, 3], # (optional) use integer or arrayref (unable to use with ArrayHashRef) SelectedValue => 'test', # (optional) use string or arrayref (unable to use with ArrayHashRef) SelectedValue => ['test', 'test1'], # (optional) use string or arrayref (unable to use with ArrayHashRef) Sort => 'NumericValue', # (optional) (AlphanumericValue|NumericValue|AlphanumericKey|NumericKey|TreeView|IndividualKey|IndividualValue) unable to use with ArrayHashRef SortIndividual => ['sec', 'min'] # (optional) only sort is set to IndividualKey or IndividualValue SortReverse => 0, # (optional) reverse the list Translation => 1, # (optional) default 1 (0|1) translate value PossibleNone => 0, # (optional) default 0 (0|1) add a leading empty selection TreeView => 0, # (optional) default 0 (0|1) DisabledBranch => 'Branch', # (optional) disable all elements of this branch (use string or arrayref) Max => 100, # (optional) default 100 max size of the shown value HTMLQuote => 0, # (optional) default 1 (0|1) disable html quote Title => 'C Text', # (optional) string will be shown as c on c OptionTitle => 1, # (optional) default 0 (0|1) show title attribute (the option value) on every option element Filters => { # (optional) filter data, used by InputFields LastOwners => { # filter id Name => 'Last owners', # name of the filter Values => { # filtered data structure Key1 => 'Value1', Key2 => 'Value2', Key3 => 'Value3', }, Active => 1, # (optional) default 0 (0|1) make this filter immediately active }, InvolvedAgents => { Name => 'Involved in this ticket', Values => \%HashWithData, }, }, ExpandFilters => 1, # (optional) default 0 (0|1) expand filters list by default ValidateDateAfter => '2016-01-01', # (optional) validate that date is after supplied value ValidateDateBefore => '2016-01-01', # (optional) validate that date is before supplied value ); my $HashRef = { Key1 => 'Value1', Key2 => 'Value2', Key3 => 'Value3', }; my $ArrayRef = [ 'KeyValue1', 'KeyValue2', 'KeyValue3', 'KeyValue4', ]; my $ArrayHashRef = [ { Key => '1', Value => 'Value1', }, { Key => '2', Value => 'Value1::Subvalue1', Selected => 1, }, { Key => '3', Value => 'Value1::Subvalue2', }, { Key => '4', Value => 'Value2', Disabled => 1, } ]; =cut sub BuildSelection { my ( $Self, %Param ) = @_; # check needed stuff for (qw(Name Data)) { if ( !$Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); return; } } # The parameters 'Ajax' and 'OnChange' are exclusive if ( $Param{Ajax} && $Param{OnChange} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "The parameters 'OnChange' and 'Ajax' exclude each other!" ); return; } # set OnChange if AJAX is used if ( $Param{Ajax} ) { if ( !$Param{Ajax}->{Depend} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Depend Param Ajax option!', ); $Self->FatalError(); } if ( !$Param{Ajax}->{Update} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Update Param Ajax option()!', ); $Self->FatalError(); } my $Selector = $Param{ID} || $Param{Name}; $Param{OnChange} = "Core.AJAX.FormUpdate(\$('#" . $Selector . "'), '" . $Param{Ajax}->{Subaction} . "'," . " '$Param{Name}'," . " ['" . join( "', '", @{ $Param{Ajax}->{Update} } ) . "']);"; } # create OptionRef my $OptionRef = $Self->_BuildSelectionOptionRefCreate(%Param); # create AttributeRef my $AttributeRef = $Self->_BuildSelectionAttributeRefCreate(%Param); # create DataRef my $DataRef = $Self->_BuildSelectionDataRefCreate( Data => $Param{Data}, AttributeRef => $AttributeRef, OptionRef => $OptionRef, ); # create FiltersRef my @Filters; my $FilterActive; if ( $Param{Filters} ) { my $Index = 1; for my $Filter ( sort keys %{ $Param{Filters} } ) { if ( $Param{Filters}->{$Filter}->{Name} && $Param{Filters}->{$Filter}->{Values} ) { my $FilterData = $Self->_BuildSelectionDataRefCreate( Data => $Param{Filters}->{$Filter}->{Values}, AttributeRef => $AttributeRef, OptionRef => $OptionRef, ); push @Filters, { Name => $Param{Filters}->{$Filter}->{Name}, Data => $FilterData, }; if ( $Param{Filters}->{$Filter}->{Active} ) { $FilterActive = $Index; } } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Each Filter must provide Name and Values!', ); $Self->FatalError(); } $Index++; } @Filters = sort { $a->{Name} cmp $b->{Name} } @Filters; } # generate output my $String = $Self->_BuildSelectionOutput( AttributeRef => $AttributeRef, DataRef => $DataRef, OptionTitle => $Param{OptionTitle}, TreeView => $Param{TreeView}, FiltersRef => \@Filters, FilterActive => $FilterActive, ExpandFilters => $Param{ExpandFilters}, ValidateDateAfter => $Param{ValidateDateAfter}, ValidateDateBefore => $Param{ValidateDateBefore}, ); return $String; } sub NoPermission { my ( $Self, %Param ) = @_; my $WithHeader = $Param{WithHeader} || 'yes'; if ( !$Param{Message} ) { $Param{Message} = $Self->{LanguageObject}->Translate( "This ticket does not exist, or you don't have permissions to access it in its current state. You can take one of the following actions:" ); } # get config option for possible next actions my $PossibleNextActions = $Kernel::OM->Get('Kernel::Config')->Get('PossibleNextActions'); POSSIBLE: if ( IsHashRefWithData($PossibleNextActions) ) { $Self->Block( Name => 'PossibleNextActionContainer', ); for my $Key ( sort keys %{$PossibleNextActions} ) { next POSSIBLE if !$Key; next POSSIBLE if !$PossibleNextActions->{$Key}; $Self->Block( Name => 'PossibleNextActionRow', Data => { Link => $Key, Description => $PossibleNextActions->{$Key}, }, ); } } # create output my $Output; $Output = $Self->Header( Title => 'Insufficient Rights' ) if ( $WithHeader eq 'yes' ); $Output .= $Self->Output( TemplateFile => 'NoPermission', Data => \%Param ); $Output .= $Self->Footer() if ( $WithHeader eq 'yes' ); # return output return $Output; } =head2 Permission() check if access to a frontend module exists my $Access = $LayoutObject->Permission( Action => 'AdminCustomerUser', Type => 'rw', # ro|rw possible ); =cut sub Permission { my ( $Self, %Param ) = @_; for my $Needed (qw(Action Type)) { if ( !defined $Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Got no $Needed!", ); $Self->FatalError(); } } # Get config option for frontend module. my $Config = $Kernel::OM->Get('Kernel::Config')->Get('Frontend::Module')->{ $Param{Action} }; return if !$Config; my $Item = $Config->{ $Param{Type} eq 'ro' ? 'GroupRo' : 'Group' }; my $GroupObject = $Kernel::OM->Get( $Self->{UserType} eq 'Customer' ? 'Kernel::System::CustomerGroup' : 'Kernel::System::Group' ); # No access restriction? if ( ref $Config->{GroupRo} eq 'ARRAY' && !scalar @{ $Config->{GroupRo} } && ref $Config->{Group} eq 'ARRAY' && !scalar @{ $Config->{Group} } ) { return 1; } # Array access restriction. elsif ( IsArrayRefWithData($Item) ) { for my $GroupName ( @{$Item} ) { return 1 if $GroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $GroupName, Type => $Param{Type}, ); } } # Allow access if there is no configuration for module group permission. elsif ( !IsArrayRefWithData( $Config->{GroupRo} ) && !IsArrayRefWithData( $Config->{Group} ) ) { return 1; } return 0; } sub CheckMimeType { my ( $Self, %Param ) = @_; my $Output = ''; if ( !$Param{Action} ) { $Param{Action} = '[% Env("Action") %]'; } # check if it is a text/plain email if ( $Param{MimeType} && $Param{MimeType} !~ /text\/plain/i ) { $Output = '

' . $Self->{LanguageObject}->Translate("This is a") . " $Param{MimeType} " . $Self->{LanguageObject}->Translate("email") . ', ' . $Self->{LanguageObject}->Translate("click here") . ' ' . $Self->{LanguageObject}->Translate("to open it in a new window.") . '

'; } # just to be compat elsif ( $Param{Body} =~ /^<.DOCTYPE\s+html|^/i ) { $Output = '

' . $Self->{LanguageObject}->Translate("This is a") . " $Param{MimeType} " . $Self->{LanguageObject}->Translate("email") . ', ' . $Self->{LanguageObject}->Translate("click here") . ' ' . $Self->{LanguageObject}->Translate("to open it in a new window.") . '

'; } # return note string return $Output; } sub ReturnValue { my ( $Self, $What ) = @_; return $Self->{$What}; } =head2 Attachment() returns browser output to display/download a attachment $HTML = $LayoutObject->Attachment( Type => 'inline', # optional, default: attachment, possible: inline|attachment Filename => 'FileName.png', # optional AdditionalHeader => $AdditionalHeader, # optional ContentType => 'image/png', Content => $Content, Sandbox => 1, # optional, default 0; use content security policy to prohibit external # scripts, flash etc. ); or for AJAX html snippets $HTML = $LayoutObject->Attachment( Type => 'inline', # optional, default: attachment, possible: inline|attachment Filename => 'FileName.html', # optional ContentType => 'text/html', Charset => 'utf-8', # optional Content => $Content, NoCache => 1, # optional ); =cut sub Attachment { my ( $Self, %Param ) = @_; # check needed params for (qw(Content ContentType)) { if ( !defined $Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Got no $_!", ); $Self->FatalError(); } } # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # return attachment my $Output = 'Content-Disposition: '; if ( $Param{Type} ) { $Output .= $Param{Type}; $Output .= '; '; } else { $Output .= $ConfigObject->Get('AttachmentDownloadType') || 'attachment'; $Output .= '; '; } if ( $Param{Filename} ) { # IE 10+ supports this my $URLEncodedFilename = URI::Escape::uri_escape_utf8( $Param{Filename} ); $Output .= " filename=\"$Param{Filename}\"; filename*=utf-8''$URLEncodedFilename"; } $Output .= "\n"; # get attachment size $Param{Size} = bytes::length( $Param{Content} ); # add no cache headers if ( $Param{NoCache} ) { $Output .= "Expires: Tue, 1 Jan 1980 12:00:00 GMT\n"; $Output .= "Cache-Control: no-cache\n"; $Output .= "Pragma: no-cache\n"; } $Output .= "Content-Length: $Param{Size}\n"; $Output .= "X-UA-Compatible: IE=edge,chrome=1\n"; if ( !$ConfigObject->Get('DisableIFrameOriginRestricted') ) { $Output .= "X-Frame-Options: SAMEORIGIN\n"; } if ( $Param{Sandbox} && !$Kernel::OM->Get('Kernel::Config')->Get('DisableContentSecurityPolicy') ) { # Disallow external and inline scripts, active content, frames, but keep allowing inline styles # as this is a common use case in emails. # Also disallow referrer headers to prevent referrer leaks via old-style policy directive. Please note this has # been deprecated and will be removed in future OTRS versions in favor of a separate header (see below). # img-src: allow external and inline (data:) images # script-src: block all scripts # object-src: allow 'self' so that the browser can load plugins for PDF display # frame-src: block all frames # style-src: allow inline styles for nice email display # referrer: don't send referrers to prevent referrer-leak attacks $Output .= "Content-Security-Policy: default-src *; img-src * data:; script-src 'none'; object-src 'self'; frame-src 'none'; style-src 'unsafe-inline'; referrer no-referrer;\n"; # Use Referrer-Policy header to suppress referrer information in modern browsers # (to prevent referrer-leak attacks). $Output .= "Referrer-Policy: no-referrer\n"; } if ( $Param{AdditionalHeader} ) { $Output .= $Param{AdditionalHeader} . "\n"; } if ( $Param{Charset} ) { $Output .= "Content-Type: $Param{ContentType}; charset=$Param{Charset};\n\n"; } else { $Output .= "Content-Type: $Param{ContentType}\n\n"; } # disable utf8 flag, to write binary to output my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode'); $EncodeObject->EncodeOutput( \$Output ); $EncodeObject->EncodeOutput( \$Param{Content} ); # fix for firefox HEAD problem if ( !$ENV{REQUEST_METHOD} || $ENV{REQUEST_METHOD} ne 'HEAD' ) { $Output .= $Param{Content}; } # reset binmode, don't use utf8 binmode STDOUT, ':bytes'; return $Output; } =head2 PageNavBar() generates a page navigation bar my %PageNavBar = $LayoutObject->PageNavBar( Limit => 100, # marks result of TotalHits red if Limit is gerater then AllHits WindowSize => 15, # max shown pages to click StartHit => 1, # start to show items PageShown => 15, # number of shown items a page AllHits => 56, # number of total hits Action => 'AgentXXX', # e. g. 'Action=' . $Self->{LayoutObject}->{Action} Link => $Link, # e. g. 'Subaction=View;' AJAXReplace => 'IDElement', # IDElement which should be replaced IDPrefix => 'Tickets', # Prefix for the id parameter ); return values of hash TotalHits # total hits Result # shown items e. g. "1-5" or "16-30" SiteNavBar # html for page nav bar e. g. "1 2 3 4" ResultLong # shown items e. g. "1-5 of 32" or "16-30 of 64" SiteNavBarLong # html for page nav bar e. g. "Page: 1 2 3 4" =cut sub PageNavBar { my ( $Self, %Param ) = @_; my $Limit = $Param{Limit} || 0; $Param{AllHits} = 0 if ( !$Param{AllHits} ); $Param{StartHit} = 0 if ( !$Param{AllHits} ); my $Pages = int( ( $Param{AllHits} / $Param{PageShown} ) + 0.99999 ); my $Page = int( ( $Param{StartHit} / $Param{PageShown} ) + 0.99999 ); my $WindowSize = $Param{WindowSize} || 5; my $IDPrefix = $Param{IDPrefix} || 'Generic'; # build Results (1-5 or 16-30) if ( $Param{AllHits} >= ( $Param{StartHit} + $Param{PageShown} ) ) { $Param{Results} = $Param{StartHit} . "-" . ( $Param{StartHit} + $Param{PageShown} - 1 ); } else { $Param{Results} = "$Param{StartHit}-$Param{AllHits}"; } # check total hits if ( $Limit == $Param{AllHits} ) { $Param{TotalHits} = "$Param{AllHits}"; } else { $Param{TotalHits} = $Param{AllHits}; } # build page nav bar my $WindowStart = sprintf( "%.0f", ( $Param{StartHit} / $Param{PageShown} ) ); $WindowStart = int( ( $WindowStart / $WindowSize ) ) + 1; $WindowStart = ( $WindowStart * $WindowSize ) - ($WindowSize); my $Action = $Param{Action} || ''; my $Link = $Param{Link} || ''; my $Baselink = "$Self->{Baselink}$Action;$Link"; my $i = 0; my %PaginationData; my $WidgetName; my $ClassWidgetName; if ( $Param{AJAXReplace} ) { $WidgetName = $Param{AJAXReplace}; $WidgetName =~ s{-}{}xmsg; $ClassWidgetName = $WidgetName; $ClassWidgetName =~ s/^Dashboard//; } while ( $i <= ( $Pages - 1 ) ) { $i++; # show normal page 1,2,3,... if ( $i <= ( $WindowStart + $WindowSize ) && $i > $WindowStart ) { my $BaselinkAll = $Baselink . "StartWindow=$WindowStart;StartHit=" . ( ( ( $i - 1 ) * $Param{PageShown} ) + 1 ); my $SelectedPage = ''; my $PageNumber = $i; if ( $Page == $i ) { $SelectedPage = 'Selected'; } if ( $Param{AJAXReplace} ) { $PaginationData{$PageNumber} = { Baselink => $BaselinkAll, AjaxReplace => $Param{AJAXReplace}, WidgetName => $ClassWidgetName }; $Self->Block( Name => 'PageAjax', Data => { BaselinkAll => $BaselinkAll, AjaxReplace => $Param{AJAXReplace}, PageNumber => $PageNumber, IDPrefix => $IDPrefix, SelectedPage => $SelectedPage, WidgetName => $ClassWidgetName }, ); } else { $Self->Block( Name => 'Page', Data => { BaselinkAll => $BaselinkAll, PageNumber => $PageNumber, IDPrefix => $IDPrefix, SelectedPage => $SelectedPage }, ); } } # over window ">>" and ">|" elsif ( $i > ( $WindowStart + $WindowSize ) ) { my $StartWindow = $WindowStart + $WindowSize + 1; my $LastStartWindow = int( $Pages / $WindowSize ); my $BaselinkOneForward = $Baselink . "StartHit=" . ( ( $i - 1 ) * $Param{PageShown} + 1 ); my $BaselinkAllForward = $Baselink . "StartHit=" . ( ( $Param{PageShown} * ( $Pages - 1 ) ) + 1 ); if ( $Param{AJAXReplace} ) { $PaginationData{$BaselinkOneForward} = { Baselink => $BaselinkOneForward, AjaxReplace => $Param{AJAXReplace}, WidgetName => $ClassWidgetName }; $PaginationData{$BaselinkAllForward} = { Baselink => $BaselinkAllForward, AjaxReplace => $Param{AJAXReplace}, WidgetName => $ClassWidgetName }; $Self->Block( Name => 'PageForwardAjax', Data => { BaselinkOneForward => $BaselinkOneForward, BaselinkAllForward => $BaselinkAllForward, AjaxReplace => $Param{AJAXReplace}, IDPrefix => $IDPrefix, WidgetName => $ClassWidgetName }, ); } else { $Self->Block( Name => 'PageForward', Data => { BaselinkOneForward => $BaselinkOneForward, BaselinkAllForward => $BaselinkAllForward, IDPrefix => $IDPrefix, }, ); } $i = 99999999; } # over window "<<" and "|<" elsif ( $i < $WindowStart && ( $i - 1 ) < $Pages ) { my $StartWindow = $WindowStart - $WindowSize - 1; my $BaselinkAllBack = $Baselink . 'StartHit=1;StartWindow=1'; my $BaselinkOneBack = $Baselink . 'StartHit=' . ( ( $WindowStart - 1 ) * ( $Param{PageShown} ) + 1 ); if ( $Param{AJAXReplace} ) { $PaginationData{$BaselinkOneBack} = { Baselink => $BaselinkOneBack, AjaxReplace => $Param{AJAXReplace}, WidgetName => $ClassWidgetName }; $PaginationData{$BaselinkAllBack} = { Baselink => $BaselinkAllBack, AjaxReplace => $Param{AJAXReplace}, WidgetName => $ClassWidgetName }; $Self->Block( Name => 'PageBackAjax', Data => { BaselinkOneBack => $BaselinkOneBack, BaselinkAllBack => $BaselinkAllBack, AjaxReplace => $Param{AJAXReplace}, IDPrefix => $IDPrefix, WidgetName => $ClassWidgetName }, ); } else { $Self->Block( Name => 'PageBack', Data => { BaselinkOneBack => $BaselinkOneBack, BaselinkAllBack => $BaselinkAllBack, IDPrefix => $IDPrefix, }, ); } $i = $WindowStart - 1; } } # send data to JS if ( $Param{AJAXReplace} ) { $Self->AddJSData( Key => 'PaginationData' . $ClassWidgetName, Value => \%PaginationData ); } $Param{SearchNavBar} = $Self->Output( TemplateFile => 'Pagination', AJAX => $Param{AJAX}, ); # only show total amount of pages if there is more than one if ( $Pages > 1 ) { $Param{NavBarLong} = "- " . $Self->{LanguageObject}->Translate("Page") . ": $Param{SearchNavBar}"; } else { $Param{SearchNavBar} = ''; } # return data return ( TotalHits => $Param{TotalHits}, Result => $Param{Results}, ResultLong => "$Param{Results} " . $Self->{LanguageObject}->Translate("of") . " $Param{TotalHits}", SiteNavBar => $Param{SearchNavBar}, SiteNavBarLong => $Param{NavBarLong}, Link => $Param{Link}, ); } sub NavigationBar { my ( $Self, %Param ) = @_; if ( !$Param{Type} ) { $Param{Type} = $Self->{ModuleReg}->{NavBarName} || 'Ticket'; } my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # Create menu items. my %NavBar; my $FrontendRegistration = $ConfigObject->Get('Frontend::Module'); my $FrontendNavigation = $ConfigObject->Get('Frontend::Navigation'); my $GroupObject = $Kernel::OM->Get('Kernel::System::Group'); MODULE: for my $Module ( sort keys %{$FrontendNavigation} ) { # Skip if module is disabled in frontend registration. next MODULE if !IsHashRefWithData( $FrontendRegistration->{$Module} ); # Top-level frontend navigation configuration should always be a hash. next MODULE if !IsHashRefWithData( $FrontendNavigation->{$Module} ); my @ModuleNavigationConfigs; # Go through all defined navigation configurations for the module and sort them by the key (00#-Module). NAVIGATION_CONFIG: for my $Key ( sort keys %{ $FrontendNavigation->{$Module} || {} } ) { next NAVIGATION_CONFIG if $Key !~ m{^\d+}; # FIXME: Support both old (HASH) and new (ARRAY of HASH) navigation configurations, for reasons of backwards # compatibility. Once we are sure everything has been migrated correctly, support for HASH-only # configuration can be dropped in future major release. if ( IsHashRefWithData( $FrontendNavigation->{$Module}->{$Key} ) ) { push @ModuleNavigationConfigs, $FrontendNavigation->{$Module}->{$Key}; } elsif ( IsArrayRefWithData( $FrontendNavigation->{$Module}->{$Key} ) ) { push @ModuleNavigationConfigs, @{ $FrontendNavigation->{$Module}->{$Key} }; } # Skip incompatible configuration. else { next NAVIGATION_CONFIG; } } ITEM: for my $Item (@ModuleNavigationConfigs) { next ITEM if !$Item->{NavBar}; $Item->{CSS} = ''; # Highlight active area link. if ( ( $Item->{Type} && $Item->{Type} eq 'Menu' ) && ( $Item->{NavBar} && $Item->{NavBar} eq $Param{Type} ) ) { $Item->{CSS} .= ' Selected'; } my $InheritPermissions = 0; # Inherit permissions from frontend registration if no permissions were defined for the navigation entry. if ( !$Item->{GroupRo} && !$Item->{Group} ) { if ( $FrontendRegistration->{GroupRo} ) { $Item->{GroupRo} = $FrontendRegistration->{GroupRo}; } if ( $FrontendRegistration->{Group} ) { $Item->{Group} = $FrontendRegistration->{Group}; } $InheritPermissions = 1; } my $Shown = 0; PERMISSION: for my $Permission (qw(GroupRo Group)) { # No access restriction. if ( ref $Item->{GroupRo} eq 'ARRAY' && !scalar @{ $Item->{GroupRo} } && ref $Item->{Group} eq 'ARRAY' && !scalar @{ $Item->{Group} } ) { $Shown = 1; last PERMISSION; } # Array access restriction. elsif ( $Item->{$Permission} && ref $Item->{$Permission} eq 'ARRAY' ) { GROUP: for my $Group ( @{ $Item->{$Permission} } ) { next GROUP if !$Group; my $HasPermission = $GroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $Group, Type => $Permission eq 'GroupRo' ? 'ro' : 'rw', ); if ($HasPermission) { $Shown = 1; last PERMISSION; } } } } # If we passed the initial permission check and didn't inherit permissions from the module registration, # make sure to also check access to the module, since navigation item might be out of sync. if ( $Shown && !$InheritPermissions ) { my $ModulePermission; PERMISSION: for my $Permission (qw(GroupRo Group)) { # No access restriction. if ( ref $FrontendRegistration->{$Module}->{GroupRo} eq 'ARRAY' && !scalar @{ $FrontendRegistration->{$Module}->{GroupRo} } && ref $FrontendRegistration->{$Module}->{Group} eq 'ARRAY' && !scalar @{ $FrontendRegistration->{$Module}->{Group} } ) { $ModulePermission = 1; last PERMISSION; } # Array access restriction. elsif ( $FrontendRegistration->{$Module}->{$Permission} && ref $FrontendRegistration->{$Module}->{$Permission} eq 'ARRAY' ) { GROUP: for my $Group ( @{ $FrontendRegistration->{$Module}->{$Permission} } ) { next GROUP if !$Group; my $HasPermission = $GroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $Group, Type => $Permission eq 'GroupRo' ? 'ro' : 'rw', ); if ($HasPermission) { $ModulePermission = 1; last PERMISSION; } } } } # Hide item if no permission was granted to access the module. if ( !$ModulePermission ) { $Shown = 0; } } next ITEM if !$Shown; # set prio of item my $Key = ( $Item->{Block} || '' ) . sprintf( "%07d", $Item->{Prio} ); COUNT: for ( 1 .. 51 ) { last COUNT if !$NavBar{$Key}; $Item->{Prio}++; $Key = ( $Item->{Block} || '' ) . sprintf( "%07d", $Item->{Prio} ); } # show as main menu if ( $Item->{Type} eq 'Menu' ) { $NavBar{$Key} = $Item; } # show as sub of main menu else { $NavBar{Sub}->{ $Item->{NavBar} }->{$Key} = $Item; } } } my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); # run menu item modules if ( ref $ConfigObject->Get('Frontend::NavBarModule') eq 'HASH' ) { my %Jobs = %{ $ConfigObject->Get('Frontend::NavBarModule') }; MENUMODULE: for my $Job ( sort keys %Jobs ) { # load module next MENUMODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, ); next MENUMODULE if !$Object; # run module %NavBar = ( %NavBar, $Object->Run( %Param, Config => $Jobs{$Job}, NavBar => \%NavBar || {} ) ); } } # show nav bar ITEM: for my $Key ( sort keys %NavBar ) { next ITEM if $Key eq 'Sub'; next ITEM if !%{ $NavBar{$Key} }; my $Item = $NavBar{$Key}; $Item->{NameForID} = $Item->{Name}; $Item->{NameForID} =~ s/[ &;]//ig; my $Sub = $NavBar{Sub}->{ $Item->{NavBar} }; $Self->Block( Name => 'ItemArea', Data => { %$Item, AccessKeyReference => $Item->{AccessKey} ? " ($Item->{AccessKey})" : '', }, ); # show sub menu (only if sub elements available) next ITEM if !$Sub; next ITEM if !keys %{$Sub}; $Self->Block( Name => 'ItemAreaSub', Data => $Item, ); for my $Key ( sort keys %{$Sub} ) { my $ItemSub = $Sub->{$Key}; $ItemSub->{NameForID} = $ItemSub->{Name}; $ItemSub->{NameForID} =~ s/[ &;]//ig; $ItemSub->{NameTop} = $Item->{NameForID}; $ItemSub->{Description} ||= $ItemSub->{Name}; # use 'name' as fallback, this is shown as the link title $Self->Block( Name => 'ItemAreaSubItem', #$Item->{Block} || 'Item', Data => { %$ItemSub, AccessKeyReference => $ItemSub->{AccessKey} ? " ($ItemSub->{AccessKey})" : '', }, ); } } # get user preferences for custom nav bar item ordering my %UserPreferences = $Kernel::OM->Get('Kernel::System::User')->GetPreferences( UserID => $Self->{UserID}, ); my $NavbarOrderItems = $UserPreferences{'UserNavBarItemsOrder'} || ''; $Self->AddJSData( Key => 'NavbarOrderItems', Value => $NavbarOrderItems, ); my $FrontendSearch = $ConfigObject->Get('Frontend::Search') || {}; my $SearchAdded; # show search icon if any search router is configured if ( IsHashRefWithData($FrontendSearch) ) { KEY: for my $Key ( sort keys %{$FrontendSearch} ) { next KEY if !IsHashRefWithData( $FrontendSearch->{$Key} ); for my $Regex ( sort keys %{ $FrontendSearch->{$Key} } ) { next KEY if !$Regex; # Check if regex matches current action. if ( $Self->{Action} =~ m{$Regex}g ) { # Extract Action from the configuration. my ($Action) = $FrontendSearch->{$Key}->{$Regex} =~ m{Action=(.*?)(;.*)?$}; # Do not show Search icon if action is not registered. next KEY if !$FrontendRegistration->{$Action}; $Self->Block( Name => 'SearchIcon', ); $SearchAdded = 1; last KEY; } } } } # If Search icon is not added, check if AgentTicketSearch is enabled and add it. if ( !$SearchAdded && $FrontendRegistration->{AgentTicketSearch} ) { $Self->Block( Name => 'SearchIcon', ); } # create & return output my $Output = $Self->Output( TemplateFile => 'AgentNavigationBar', Data => \%Param, ); # run nav bar output modules my $NavBarOutputModuleConfig = $ConfigObject->Get('Frontend::NavBarOutputModule'); if ( ref $NavBarOutputModuleConfig eq 'HASH' ) { my %Jobs = %{$NavBarOutputModuleConfig}; OUTPUTMODULE: for my $Job ( sort keys %Jobs ) { # load module next OUTPUTMODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, ); next OUTPUTMODULE if !$Object; # run module $Output .= $Object->Run( %Param, Config => $Jobs{$Job} ); } } # run notification modules my $FrontendNotifyModuleConfig = $ConfigObject->Get('Frontend::NotifyModule'); if ( ref $FrontendNotifyModuleConfig eq 'HASH' ) { my %Jobs = %{$FrontendNotifyModuleConfig}; NOTIFICATIONMODULE: for my $Job ( sort keys %Jobs ) { # load module next NOTIFICATIONMODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, ); next NOTIFICATIONMODULE if !$Object; # run module $Output .= $Object->Run( %Param, Config => $Jobs{$Job} ); } } # run nav bar modules if ( $Self->{NavigationModule} ) { # run navbar modules my %Jobs = %{ $Self->{NavigationModule} }; # load module if ( !$MainObject->Require( $Jobs{Module} ) ) { return $Output; } my $Object = $Jobs{Module}->new( %{$Self}, LayoutObject => $Self, ); if ( !$Object ) { return $Output; } # run module $Output .= $Object->Run( %Param, Config => \%Jobs ); } return $Output; } sub TransformDateSelection { my ( $Self, %Param ) = @_; # get key prefix my $Prefix = $Param{Prefix} || ''; # time zone translation if needed # from user time zone to OTRS time zone if ( $Self->{UserTimeZone} ) { my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { Year => $Param{ $Prefix . 'Year' }, Month => $Param{ $Prefix . 'Month' }, Day => $Param{ $Prefix . 'Day' }, Hour => $Param{ $Prefix . 'Hour' } || 0, Minute => $Param{ $Prefix . 'Minute' } || 0, Second => $Param{ $Prefix . 'Second' } || 0, TimeZone => $Self->{UserTimeZone}, }, ); if ($DateTimeObject) { $DateTimeObject->ToOTRSTimeZone(); my $DateTimeValues = $DateTimeObject->Get(); $Param{ $Prefix . 'Year' } = $DateTimeValues->{Year}; $Param{ $Prefix . 'Month' } = $DateTimeValues->{Month}; $Param{ $Prefix . 'Day' } = $DateTimeValues->{Day}; $Param{ $Prefix . 'Hour' } = $DateTimeValues->{Hour}; $Param{ $Prefix . 'Minute' } = $DateTimeValues->{Minute}; $Param{ $Prefix . 'Second' } = $DateTimeValues->{Second}; } } # reset prefix $Param{Prefix} = ''; return %Param; } =head2 BuildDateSelection() build the HTML code to represent a date selection based on the given data. Depending on the SysConfig settings the controls to set the date could be multiple select or input fields my $HTML = $LayoutObject->BuildDateSelection( Prefix => 'some prefix', # optional, (needed to specify other parameters) Year => 2015, # optional, defaults to current year, used to set the initial value Month => 6, # optional, defaults to current month, used to set the initial value Day => 9, # optional, defaults to current day, used to set the initial value Hour => 12, # optional, defaults to current hour, used to set the initial value Minute => 26, # optional, defaults to current minute, used to set the initial value Second => 59, # optional, defaults to current second, used to set the initial value Optional => 1, # optional, default 0, when active a checkbox is included to specify # if the values should be saved or not Used => 1, # optional, default 0, used to set the initial state of the checkbox # mentioned above Required => 1, # optional, default 0 (Deprecated) Class => 'some class', # optional, specify an additional class to the HTML elements Area => 'some area', # optional, default 'Agent' (Deprecated) DiffTime => 123, # optional, default 0, used to set the initial time influencing the # current time (in seconds) OverrideTimeZone => 1, # optional (1 or 0), when active the time is not translated to the user # time zone YearPeriodFuture => 3, # optional, used to define the number of years in future to be display # in the year select YearPeriodPast => 2, # optional, used to define the number of years in past to be display # in the year select YearDiff => 0, # optional. used to define the number of years to be displayed # in the year select (alternatively to YearPeriodFuture and YearPeriodPast) ValidateDateInFuture => 1, # optional (1 or 0), when active sets an special class to validate # that the date set in the controls to be in the future ValidateDateNotInFuture => 1, # optional (1 or 0), when active sets an special class to validate # that the date set in the controls not to be in the future ValidateDateAfterPrefix => 'Start', # optional (Prefix), when defined sets a special class to validate # that the date set in the controls comes after the date with Prefix ValidateDateAfterValue => '2016-01-01', # optional (Date), when defined sets a special data parameter to validate # that the date set in the controls comes after the supplied date ValidateDateBeforePrefix => 'End', # optional (Prefix), when defined sets a special class to validate # that the date set in the controls comes before the date with Prefix ValidateDateBeforeValue => '2016-01-01', # optional (Date), when defined sets a special data parameter to validate # that the date set in the controls comes before the supplied date Calendar => 2, # optional, used to define the SysConfig calendar on which the Datepicker # will be based on to show the vacation days and the start week day Format => 'DateInputFormat', # optional, or 'DateInputFormatLong', used to define if only date or # date/time components should be shown (DateInputFormatLong shows date/time) Validate => 1, # optional (1 or 0), defines if the date selection should be validated on # client side with JS Disabled => 1, # optional (1 or 0), when active select and checkbox controls gets the # disabled attribute and input fields gets the read only attribute ); =cut sub BuildDateSelection { my ( $Self, %Param ) = @_; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $DateInputStyle = $ConfigObject->Get('TimeInputFormat'); my $MinuteStep = $ConfigObject->Get('TimeInputMinutesStep'); my $Prefix = $Param{Prefix} || ''; my $DiffTime = $Param{DiffTime} || 0; my $Format = defined( $Param{Format} ) ? $Param{Format} : 'DateInputFormatLong'; my $Area = $Param{Area} || 'Agent'; my $Optional = $Param{ $Prefix . 'Optional' } || 0; my $Required = $Param{ $Prefix . 'Required' } || 0; my $Used = $Param{ $Prefix . 'Used' } || 0; my $Class = $Param{ $Prefix . 'Class' } || ''; # Defines, if the date selection should be validated on client side with JS my $Validate = $Param{Validate} || 0; # Validate that the date is in the future (e. g. pending times) my $ValidateDateInFuture = $Param{ValidateDateInFuture} || 0; my $ValidateDateNotInFuture = $Param{ValidateDateNotInFuture} || 0; # Validate that the date is set after/before supplied date my $ValidateDateAfterPrefix = $Param{ValidateDateAfterPrefix} || ''; my $ValidateDateAfterValue = $Param{ValidateDateAfterValue} || ''; my $ValidateDateBeforePrefix = $Param{ValidateDateBeforePrefix} || ''; my $ValidateDateBeforeValue = $Param{ValidateDateBeforeValue} || ''; my $GetCurSysDTUnitFromLowest = sub { my %Param = @_; my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { TimeZone => $Self->{UserTimeZone} } ); if ( $Param{AddSeconds} ) { $DateTimeObject->Add( Seconds => $Param{AddSeconds} ); } my %Details = %{ $DateTimeObject->Get() }; return map { $Details{$_} } (qw(Second Minute Hour Day Month Year)); }; my ( $s, $m, $h, $D, $M, $Y ) = $GetCurSysDTUnitFromLowest->( AddSeconds => $DiffTime, ); my ( $Cs, $Cm, $Ch, $CD, $CM, $CY ) = $GetCurSysDTUnitFromLowest->(); # time zone translation if ( $Self->{UserTimeZone} && $Param{ $Prefix . 'Year' } && $Param{ $Prefix . 'Month' } && $Param{ $Prefix . 'Day' } && !$Param{OverrideTimeZone} ) { my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { Year => $Param{ $Prefix . 'Year' }, Month => $Param{ $Prefix . 'Month' }, Day => $Param{ $Prefix . 'Day' }, Hour => $Param{ $Prefix . 'Hour' } || 0, Minute => $Param{ $Prefix . 'Minute' } || 0, Second => $Param{ $Prefix . 'Second' } || 0, }, ); if ($DateTimeObject) { $DateTimeObject->ToTimeZone( TimeZone => $Self->{UserTimeZone} ); my $DateTimeValues = $DateTimeObject->Get(); $Param{ $Prefix . 'Year' } = $DateTimeValues->{Year}; $Param{ $Prefix . 'Month' } = $DateTimeValues->{Month}; $Param{ $Prefix . 'Day' } = $DateTimeValues->{Day}; $Param{ $Prefix . 'Hour' } = $DateTimeValues->{Hour}; $Param{ $Prefix . 'Minute' } = $DateTimeValues->{Minute}; $Param{ $Prefix . 'Second' } = $DateTimeValues->{Second}; } } # year if ( $DateInputStyle eq 'Option' ) { my %Year; if ( defined $Param{YearPeriodPast} && defined $Param{YearPeriodFuture} ) { for ( $Y - $Param{YearPeriodPast} .. $Y + $Param{YearPeriodFuture} ) { $Year{$_} = $_; } } else { for ( 2001 .. $Y + 1 + ( $Param{YearDiff} || 0 ) ) { $Year{$_} = $_; } } # Check if the DiffTime is in a future year. In this case, we add the missing years between # $CY (current year) and $Y (year) to allow the user to manually set back the year if needed. if ( $Y > $CY ) { for ( $CY .. $Y ) { $Year{$_} = $_; } } $Param{Year} = $Self->BuildSelection( Name => $Prefix . 'Year', Data => \%Year, SelectedID => int( $Param{ $Prefix . 'Year' } || $Y ), Translation => 0, Class => $Validate ? "Validate_DateYear $Class" : $Class, Title => $Self->{LanguageObject}->Translate('Year'), Disabled => $Param{Disabled}, ); } else { $Param{Year} = "{LanguageObject}->Translate('Year') . "\" value=\"" . sprintf( "%02d", ( $Param{ $Prefix . 'Year' } || $Y ) ) . "\" " . ( $Param{Disabled} ? 'readonly="readonly"' : '' ) . "/>"; } # month if ( $DateInputStyle eq 'Option' ) { my %Month = map { $_ => sprintf( "%02d", $_ ); } ( 1 .. 12 ); $Param{Month} = $Self->BuildSelection( Name => $Prefix . 'Month', Data => \%Month, SelectedID => int( $Param{ $Prefix . 'Month' } || $M ), Translation => 0, Class => $Validate ? "Validate_DateMonth $Class" : $Class, Title => $Self->{LanguageObject}->Translate('Month'), Disabled => $Param{Disabled}, ); } else { $Param{Month} = "{LanguageObject}->Translate('Month') . "\" value=\"" . sprintf( "%02d", ( $Param{ $Prefix . 'Month' } || $M ) ) . "\" " . ( $Param{Disabled} ? 'readonly="readonly"' : '' ) . "/>"; } my $DateValidateClasses = ''; if ($Validate) { $DateValidateClasses .= "Validate_DateDay Validate_DateYear_${Prefix}Year Validate_DateMonth_${Prefix}Month"; if ( $Format eq 'DateInputFormatLong' ) { $DateValidateClasses .= " Validate_DateHour_${Prefix}Hour Validate_DateMinute_${Prefix}Minute"; } if ($ValidateDateInFuture) { $DateValidateClasses .= " Validate_DateInFuture"; } if ($ValidateDateNotInFuture) { $DateValidateClasses .= " Validate_DateNotInFuture"; } if ( $ValidateDateAfterPrefix || $ValidateDateAfterValue ) { $DateValidateClasses .= ' Validate_DateAfter'; } if ( $ValidateDateBeforePrefix || $ValidateDateBeforeValue ) { $DateValidateClasses .= ' Validate_DateBefore'; } if ($ValidateDateAfterPrefix) { $DateValidateClasses .= " Validate_DateAfter_$ValidateDateAfterPrefix"; } if ($ValidateDateBeforePrefix) { $DateValidateClasses .= " Validate_DateBefore_$ValidateDateBeforePrefix"; } } # day if ( $DateInputStyle eq 'Option' ) { my %Day = map { $_ => sprintf( "%02d", $_ ); } ( 1 .. 31 ); $Param{Day} = $Self->BuildSelection( Name => $Prefix . 'Day', Data => \%Day, SelectedID => int( $Param{ $Prefix . 'Day' } || $D ), Translation => 0, Class => "$DateValidateClasses $Class", Title => $Self->{LanguageObject}->Translate('Day'), Disabled => $Param{Disabled}, ValidateDateAfter => $ValidateDateAfterValue, ValidateDateBefore => $ValidateDateBeforeValue, ); } else { $Param{Day} = "{LanguageObject}->Translate('Day') . "\" value=\"" . sprintf( "%02d", ( $Param{ $Prefix . 'Day' } || $D ) ) . "\" " . ( $Param{Disabled} ? 'readonly="readonly"' : '' ) . "/>"; } if ( $Format eq 'DateInputFormatLong' ) { # hour if ( $DateInputStyle eq 'Option' ) { my %Hour = map { $_ => sprintf( "%02d", $_ ); } ( 0 .. 23 ); $Param{Hour} = $Self->BuildSelection( Name => $Prefix . 'Hour', Data => \%Hour, SelectedID => defined( $Param{ $Prefix . 'Hour' } ) ? int( $Param{ $Prefix . 'Hour' } ) : int($h), Translation => 0, Class => $Validate ? ( 'Validate_DateHour ' . $Class ) : $Class, Title => $Self->{LanguageObject}->Translate('Hours'), Disabled => $Param{Disabled}, ); } else { $Param{Hour} = "{LanguageObject}->Translate('Hours') . "\" value=\"" . sprintf( "%02d", ( defined( $Param{ $Prefix . 'Hour' } ) ? int( $Param{ $Prefix . 'Hour' } ) : $h ) ) . "\" " . ( $Param{Disabled} ? 'readonly="readonly"' : '' ) . "/>"; } # minute if ( $DateInputStyle eq 'Option' ) { my %Minute = map { $_ => sprintf( "%02d", $_ ); } map { $_ * $MinuteStep } ( 0 .. ( 60 / $MinuteStep - 1 ) ); $Param{Minute} = $Self->BuildSelection( Name => $Prefix . 'Minute', Data => \%Minute, SelectedID => defined( $Param{ $Prefix . 'Minute' } ) ? int( $Param{ $Prefix . 'Minute' } ) : int( $m - $m % $MinuteStep ), Translation => 0, Class => $Validate ? ( 'Validate_DateMinute ' . $Class ) : $Class, Title => $Self->{LanguageObject}->Translate('Minutes'), Disabled => $Param{Disabled}, ); } else { $Param{Minute} = "{LanguageObject}->Translate('Minutes') . "\" value=\"" . sprintf( "%02d", ( defined( $Param{ $Prefix . 'Minute' } ) ? int( $Param{ $Prefix . 'Minute' } ) : $m ) ) . "\" " . ( $Param{Disabled} ? 'readonly="readonly"' : '' ) . "/>"; } } # Get first day of the week my $WeekDayStart = $ConfigObject->Get('CalendarWeekDayStart'); if ( $Param{Calendar} ) { if ( $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" ) ) { $WeekDayStart = $ConfigObject->Get( "CalendarWeekDayStart::Calendar" . $Param{Calendar} ); } } if ( !defined $WeekDayStart ) { $WeekDayStart = 1; } my $Output; # optional checkbox if ($Optional) { my $Checked = ''; if ($Used) { $Checked = ' checked="checked"'; } $Output .= "{LanguageObject}->Translate('Check to activate this date') . "\" " . ( $Param{Disabled} ? 'disabled="disabled"' : '' ) . "/> "; } # remove 'Second' because it is never used and bug #9441 delete $Param{ $Prefix . 'Second' }; # date format $Output .= $Self->{LanguageObject}->Time( Action => 'Return', Format => 'DateInputFormat', Mode => 'NotNumeric', %Param, ); # prepare datepicker for specific calendar my $VacationDays = ''; if ( $Param{Calendar} ) { $VacationDays = $Self->DatepickerGetVacationDays( Calendar => $Param{Calendar}, ); } my $VacationDaysJSON = $Self->JSONEncode( Data => $VacationDays, ); # Add Datepicker JS to output. my $DatepickerJS = ' Core.UI.Datepicker.Init({ Day: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Day"), Month: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Month"), Year: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Year"), Hour: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Hour"), Minute: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Minute"), VacationDays: ' . $VacationDaysJSON . ', DateInFuture: ' . ( $ValidateDateInFuture ? 'true' : 'false' ) . ', DateNotInFuture: ' . ( $ValidateDateNotInFuture ? 'true' : 'false' ) . ', WeekDayStart: ' . $WeekDayStart . ' });'; $Self->AddJSOnDocumentComplete( Code => $DatepickerJS ); $Self->{HasDatepicker} = 1; # Call some Datepicker init code. return $Output; } =head2 HumanReadableDataSize() Produces human readable data size. my $SizeStr = $LayoutObject->HumanReadableDataSize( Size => 123, # size in bytes ); Returns $SizeStr = '123 B'; # example with decimal point: 123.4 MB =cut sub HumanReadableDataSize { my ( $Self, %Param ) = @_; # Use simple string concatenation to format real number. "sprintf" uses dot (.) as decimal separator unless # locale and POSIX (LC_NUMERIC) is used. Even in this case, you are not allowed to use custom separator # (as defined in language files). my $FormatSize = sub { my ($Number) = @_; my $ReadableSize; if ( IsInteger($Number) ) { $ReadableSize = $Number; } else { # Get integer and decimal parts. my ( $Integer, $Float ) = split( m{\.}, sprintf( "%.1f", $Number ) ); my $Separator = $Self->{LanguageObject}->{DecimalSeparator} || '.'; # Format size with provided decimal separator. $ReadableSize = $Integer . $Separator . $Float; } return $ReadableSize; }; if ( !defined( $Param{Size} ) ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Size!', ); return; } if ( !IsInteger( $Param{Size} ) ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Size must be integer!', ); return; } # Use convention described on https://en.wikipedia.org/wiki/File_size my ( $SizeStr, $ReadableSize ); if ( $Param{Size} >= ( 1024**4 ) ) { $ReadableSize = $FormatSize->( $Param{Size} / ( 1024**4 ) ); $SizeStr = $Self->{LanguageObject}->Translate( '%s TB', $ReadableSize ); } elsif ( $Param{Size} >= ( 1024**3 ) ) { $ReadableSize = $FormatSize->( $Param{Size} / ( 1024**3 ) ); $SizeStr = $Self->{LanguageObject}->Translate( '%s GB', $ReadableSize ); } elsif ( $Param{Size} >= ( 1024**2 ) ) { $ReadableSize = $FormatSize->( $Param{Size} / ( 1024**2 ) ); $SizeStr = $Self->{LanguageObject}->Translate( '%s MB', $ReadableSize ); } elsif ( $Param{Size} >= 1024 ) { $ReadableSize = $FormatSize->( $Param{Size} / 1024 ); $SizeStr = $Self->{LanguageObject}->Translate( '%s KB', $ReadableSize ); } else { $SizeStr = $Self->{LanguageObject}->Translate( '%s B', $Param{Size} ); } return $SizeStr; } sub CustomerLogin { my ( $Self, %Param ) = @_; my $Output = ''; $Param{TitleArea} = $Self->{LanguageObject}->Translate('Login') . ' - '; # set Action parameter for the loader $Self->{Action} = 'CustomerLogin'; $Param{IsLoginPage} = 1; $Param{'XLoginHeader'} = 1; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); if ( $ConfigObject->Get('SessionUseCookie') ) { # always set a cookie, so that at the time the user submits # the password, we know already if the browser supports cookies. # ( the session cookie isn't available at that time ). my $CookieSecureAttribute = 0; if ( $ConfigObject->Get('HttpType') eq 'https' ) { # Restrict Cookie to HTTPS if it is used. $CookieSecureAttribute = 1; } $Self->{SetCookies}->{OTRSBrowserHasCookie} = $Kernel::OM->Get('Kernel::System::Web::Request')->SetCookie( Key => 'OTRSBrowserHasCookie', Value => 1, Expires => '+1y', Path => $ConfigObject->Get('ScriptAlias'), Secure => $CookieSecureAttribute, HttpOnly => 1, ); } # add cookies if exists if ( $Self->{SetCookies} && $ConfigObject->Get('SessionUseCookie') ) { for ( sort keys %{ $Self->{SetCookies} } ) { $Output .= "Set-Cookie: $Self->{SetCookies}->{$_}\n"; } } # check if message should be shown if ( $Param{Message} ) { $Self->Block( Name => 'Message', Data => \%Param, ); } # Generate the minified CSS and JavaScript files and the tags referencing them (see LayoutLoader) $Self->LoaderCreateCustomerCSSCalls(); $Self->LoaderCreateCustomerJSCalls(); $Self->LoaderCreateJavaScriptTranslationData(); $Self->LoaderCreateJavaScriptTemplateData(); my $OTRSBusinessObject = $Kernel::OM->Get('Kernel::System::OTRSBusiness'); $Param{OTRSBusinessIsInstalled} = $OTRSBusinessObject->OTRSBusinessIsInstalled(); $Param{OTRSSTORMIsInstalled} = $OTRSBusinessObject->OTRSSTORMIsInstalled(); $Param{OTRSCONTROLIsInstalled} = $OTRSBusinessObject->OTRSCONTROLIsInstalled(); $Self->AddJSData( Key => 'Baselink', Value => $Self->{Baselink}, ); # Add header logo, if configured if ( defined $ConfigObject->Get('CustomerLogo') ) { my %CustomerLogo = %{ $ConfigObject->Get('CustomerLogo') }; my %Data; for my $CSSStatement ( sort keys %CustomerLogo ) { if ( $CSSStatement eq 'URL' ) { my $WebPath = ''; if ( $CustomerLogo{$CSSStatement} !~ /(http|ftp|https):\//i ) { $WebPath = $ConfigObject->Get('Frontend::WebPath'); } $Data{'URL'} = 'url(' . $WebPath . $CustomerLogo{$CSSStatement} . ')'; } else { $Data{$CSSStatement} = $CustomerLogo{$CSSStatement}; } } $Self->Block( Name => 'HeaderLogoCSS', Data => \%Data, ); $Self->Block( Name => 'HeaderLogo', ); } # get system maintenance object my $SystemMaintenanceObject = $Kernel::OM->Get('Kernel::System::SystemMaintenance'); my $ActiveMaintenance = $SystemMaintenanceObject->SystemMaintenanceIsActive(); # check if system maintenance is active if ($ActiveMaintenance) { my $SystemMaintenanceData = $SystemMaintenanceObject->SystemMaintenanceGet( ID => $ActiveMaintenance, UserID => 1, ); if ( $SystemMaintenanceData->{ShowLoginMessage} ) { my $LoginMessage = $SystemMaintenanceData->{LoginMessage} || $ConfigObject->Get('SystemMaintenance::IsActiveDefaultLoginMessage') || "System maintenance is active, not possible to perform a login!"; $Self->Block( Name => 'SystemMaintenance', Data => { LoginMessage => $LoginMessage, }, ); } } # show prelogin block, if in prelogin mode (e.g. SSO login) if ( defined $Param{'Mode'} && $Param{'Mode'} eq 'PreLogin' ) { $Self->Block( Name => 'PreLogin', Data => \%Param, ); } # if not in PreLogin mode, show normal login form else { $Self->Block( Name => 'LoginBox', Data => \%Param, ); # show 2 factor password input if we have at least one backend enabled COUNT: for my $Count ( '', 1 .. 10 ) { next COUNT if !$ConfigObject->Get("Customer::AuthTwoFactorModule$Count"); $Self->Block( Name => 'AuthTwoFactor', Data => \%Param, ); last COUNT; } # get lost password output if ( $ConfigObject->Get('CustomerPanelLostPassword') && $ConfigObject->Get('Customer::AuthModule') eq 'Kernel::System::CustomerAuth::DB' ) { $Self->Block( Name => 'LostPasswordLink', Data => \%Param, ); $Self->Block( Name => 'LostPassword', Data => \%Param, ); } # get create account output if ( $ConfigObject->Get('CustomerPanelCreateAccount') && $ConfigObject->Get('Customer::AuthModule') eq 'Kernel::System::CustomerAuth::DB' ) { $Self->Block( Name => 'CreateAccountLink', Data => \%Param, ); $Self->Block( Name => 'CreateAccount', Data => \%Param, ); } } # send data to JS $Self->AddJSData( Key => 'LoginFailed', Value => $Param{LoginFailed}, ); # Display footer links. my $FooterLinks = $ConfigObject->Get('PublicFrontend::FooterLinks'); if ( IsHashRefWithData($FooterLinks) ) { my @FooterLinks; for my $Link ( sort keys %{$FooterLinks} ) { push @FooterLinks, { Description => $FooterLinks->{$Link}, Target => $Link, }; } $Param{FooterLinks} = \@FooterLinks; } # create & return output $Output .= $Self->Output( TemplateFile => 'CustomerLogin', Data => \%Param, ); # remove the version tag from the header if configured $Self->_DisableBannerCheck( OutputRef => \$Output ); return $Output; } sub CustomerHeader { my ( $Self, %Param ) = @_; my $Type = $Param{Type} || ''; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # add cookies if exists my $Output = ''; if ( $Self->{SetCookies} && $ConfigObject->Get('SessionUseCookie') ) { for ( sort keys %{ $Self->{SetCookies} } ) { $Output .= "Set-Cookie: $Self->{SetCookies}->{$_}\n"; } } my $File = $Param{Filename} || $Self->{Action} || 'unknown'; # set file name for "save page as" $Param{ContentDisposition} = "filename=\"$File.html\""; # area and title if ( !$Param{Area} && $ConfigObject->Get('CustomerFrontend::Module')->{ $Self->{Action} } ) { $Param{Area} = $ConfigObject->Get('CustomerFrontend::Module')->{ $Self->{Action} } ->{NavBarName} || ''; } if ( !$Param{Title} && $ConfigObject->Get('CustomerFrontend::Module')->{ $Self->{Action} } ) { $Param{Title} = $ConfigObject->Get('CustomerFrontend::Module')->{ $Self->{Action} }->{Title} || ''; } if ( !$Param{Area} && $ConfigObject->Get('PublicFrontend::Module')->{ $Self->{Action} } ) { $Param{Area} = $ConfigObject->Get('PublicFrontend::Module')->{ $Self->{Action} } ->{NavBarName} || ''; } if ( !$Param{Title} && $ConfigObject->Get('PublicFrontend::Module')->{ $Self->{Action} } ) { $Param{Title} = $ConfigObject->Get('PublicFrontend::Module')->{ $Self->{Action} }->{Title} || ''; } for my $Word (qw(Value Title Area)) { if ( $Param{$Word} ) { $Param{TitleArea} .= $Self->{LanguageObject}->Translate( $Param{$Word} ) . ' - '; } } my $Frontend; if ( $ConfigObject->Get('CustomerFrontend::Module')->{ $Self->{Action} } ) { $Frontend = 'Customer'; } else { $Frontend = 'Public'; } # run header meta modules for customer and public frontends my $HeaderMetaModule = $ConfigObject->Get( $Frontend . 'Frontend::HeaderMetaModule' ); if ( ref $HeaderMetaModule eq 'HASH' ) { my %Jobs = %{$HeaderMetaModule}; my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); MODULE: for my $Job ( sort keys %Jobs ) { # load and run module next MODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self ); next MODULE if !$Object; $Object->Run( %Param, Config => $Jobs{$Job} ); } } # set rtl if needed if ( $Self->{TextDirection} && $Self->{TextDirection} eq 'rtl' ) { $Param{BodyClass} = 'RTL'; } elsif ( $ConfigObject->Get('Frontend::DebugMode') ) { $Self->Block( Name => 'DebugRTLButton', ); } # Add header logo, if configured if ( defined $ConfigObject->Get('CustomerLogo') ) { my %CustomerLogo = %{ $ConfigObject->Get('CustomerLogo') }; my %Data; for my $CSSStatement ( sort keys %CustomerLogo ) { if ( $CSSStatement eq 'URL' ) { my $WebPath = ''; if ( $CustomerLogo{$CSSStatement} !~ /(http|ftp|https):\//i ) { $WebPath = $ConfigObject->Get('Frontend::WebPath'); } $Data{'URL'} = 'url(' . $WebPath . $CustomerLogo{$CSSStatement} . ')'; } else { $Data{$CSSStatement} = $CustomerLogo{$CSSStatement}; } } $Self->Block( Name => 'HeaderLogoCSS', Data => \%Data, ); $Self->Block( Name => 'HeaderLogo', ); } # Generate the minified CSS and JavaScript files # and the tags referencing them (see LayoutLoader) $Self->LoaderCreateCustomerCSSCalls(); # create & return output $Output .= $Self->Output( TemplateFile => "CustomerHeader$Type", Data => \%Param, ); # remove the version tag from the header if configured $Self->_DisableBannerCheck( OutputRef => \$Output ); return $Output; } sub CustomerFooter { my ( $Self, %Param ) = @_; my $Type = $Param{Type} || ''; my $HasDatepicker = $Self->{HasDatepicker} || 0; # Generate the minified CSS and JavaScript files # and the tags referencing them (see LayoutLoader) $Self->LoaderCreateCustomerJSCalls(); $Self->LoaderCreateJavaScriptTranslationData(); $Self->LoaderCreateJavaScriptTemplateData(); # get datepicker data, if needed in module if ($HasDatepicker) { my $VacationDays = $Self->DatepickerGetVacationDays(); my $TextDirection = $Self->{LanguageObject}->{TextDirection} || ''; # send data to JS $Self->AddJSData( Key => 'Datepicker', Value => { VacationDays => $VacationDays, IsRTL => ( $TextDirection eq 'rtl' ) ? 1 : 0, }, ); } my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # Check if video chat is enabled. if ( $Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::VideoChat', Silent => 1 ) ) { $Param{VideoChatEnabled} = $Kernel::OM->Get('Kernel::System::VideoChat')->IsEnabled() || $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => 'UnitTestMode' ) // 0; } # Check if customer user has permission for chat. my $CustomerChatPermission; if ( $Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::Chat', Silent => 1 ) ) { my $CustomerChatConfig = $ConfigObject->Get('CustomerFrontend::Module')->{'CustomerChat'} || {}; if ( $Kernel::OM->Get('Kernel::Config')->Get('CustomerGroupSupport') && ( IsArrayRefWithData( $CustomerChatConfig->{GroupRo} ) || IsArrayRefWithData( $CustomerChatConfig->{Group} ) ) ) { my $CustomerGroupObject = $Kernel::OM->Get('Kernel::System::CustomerGroup'); GROUP: for my $GroupName ( @{ $CustomerChatConfig->{GroupRo} }, @{ $CustomerChatConfig->{Group} } ) { $CustomerChatPermission = $CustomerGroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $GroupName, Type => 'ro', ); last GROUP if $CustomerChatPermission; } } else { $CustomerChatPermission = 1; } } # don't check for business package if the database was not yet configured (in the installer) if ( $ConfigObject->Get('SecureMode') ) { my $OTRSBusinessObject = $Kernel::OM->Get('Kernel::System::OTRSBusiness'); $Param{OTRSBusinessIsInstalled} = $OTRSBusinessObject->OTRSBusinessIsInstalled(); $Param{OTRSSTORMIsInstalled} = $OTRSBusinessObject->OTRSSTORMIsInstalled(); $Param{OTRSCONTROLIsInstalled} = $OTRSBusinessObject->OTRSCONTROLIsInstalled(); } # AutoComplete-Config my $AutocompleteConfig = $ConfigObject->Get('AutoComplete::Customer'); for my $ConfigElement ( sort keys %{$AutocompleteConfig} ) { $AutocompleteConfig->{$ConfigElement}->{ButtonText} = $Self->{LanguageObject}->Translate( $AutocompleteConfig->{$ConfigElement}{ButtonText} ); } # add JS data my %JSConfig = ( Baselink => $Self->{Baselink}, CGIHandle => $Self->{CGIHandle}, WebPath => $ConfigObject->Get('Frontend::WebPath'), Action => $Self->{Action}, Subaction => $Self->{Subaction}, SessionIDCookie => $Self->{SessionIDCookie}, SessionName => $Self->{SessionName}, SessionID => $Self->{SessionID}, SessionUseCookie => $ConfigObject->Get('SessionUseCookie'), ChallengeToken => $Self->{UserChallengeToken}, CustomerPanelSessionName => $ConfigObject->Get('CustomerPanelSessionName'), UserLanguage => $Self->{UserLanguage}, CheckEmailAddresses => $ConfigObject->Get('CheckEmailAddresses'), OTRSBusinessIsInstalled => $Param{OTRSBusinessIsInstalled}, OTRSSTORMIsInstalled => $Param{OTRSSTORMIsInstalled}, OTRSCONTROLIsInstalled => $Param{OTRSCONTROLIsInstalled}, InputFieldsActivated => $ConfigObject->Get('ModernizeCustomerFormFields'), Autocomplete => $AutocompleteConfig, VideoChatEnabled => $Param{VideoChatEnabled}, WebMaxFileUpload => $ConfigObject->Get('WebMaxFileUpload'), CustomerChatPermission => $CustomerChatPermission, ); for my $Config ( sort keys %JSConfig ) { $Self->AddJSData( Key => $Config, Value => $JSConfig{$Config}, ); } # Display footer links. my $FooterLinks = $ConfigObject->Get('PublicFrontend::FooterLinks'); if ( IsHashRefWithData($FooterLinks) ) { my @FooterLinks; for my $Link ( sort keys %{$FooterLinks} ) { push @FooterLinks, { Description => $FooterLinks->{$Link}, Target => $Link, }; } $Param{FooterLinks} = \@FooterLinks; } # create & return output return $Self->Output( TemplateFile => "CustomerFooter$Type", Data => \%Param, ); } sub CustomerFatalError { my ( $Self, %Param ) = @_; # Prevent endless recursion in case of problems with Template engine. return if ( $Self->{InFatalError}++ ); if ( $Param{Message} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Caller => 1, Priority => 'error', Message => $Param{Message}, ); } my $Output = $Self->CustomerHeader( Area => 'Frontend', Title => 'Fatal Error' ); $Output .= $Self->CustomerError(%Param); $Output .= $Self->CustomerFooter(); $Self->Print( Output => \$Output ); exit; } sub CustomerNavigationBar { my ( $Self, %Param ) = @_; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # create menu items my %NavBarModule; my $FrontendModule = $ConfigObject->Get('CustomerFrontend::Module'); my $NavigationConfig = $ConfigObject->Get('CustomerFrontend::Navigation'); my $GroupObject = $Kernel::OM->Get('Kernel::System::CustomerGroup'); MODULE: for my $Module ( sort keys %{$NavigationConfig} ) { # Skip if module is disabled in frontend registration. next MODULE if !IsHashRefWithData( $FrontendModule->{$Module} ); # Top-level frontend navigation configuration should always be a hash. next MODULE if !IsHashRefWithData( $NavigationConfig->{$Module} ); my @ModuleNavigationConfigs; # Go through all defined navigation configurations for the module and sort them by the key (00#-Module). NAVIGATION_CONFIG: for my $Key ( sort keys %{ $NavigationConfig->{$Module} || {} } ) { next NAVIGATION_CONFIG if $Key !~ m{^\d+}; # FIXME: Support both old (HASH) and new (ARRAY of HASH) navigation configurations, for reasons of backwards # compatibility. Once we are sure everything has been migrated correctly, support for HASH-only # configuration can be dropped in future major release. if ( IsHashRefWithData( $NavigationConfig->{$Module}->{$Key} ) ) { push @ModuleNavigationConfigs, $NavigationConfig->{$Module}->{$Key}; } elsif ( IsArrayRefWithData( $NavigationConfig->{$Module}->{$Key} ) ) { push @ModuleNavigationConfigs, @{ $NavigationConfig->{$Module}->{$Key} }; } # Skip incompatible configuration. else { next NAVIGATION_CONFIG; } } ITEM: for my $Item (@ModuleNavigationConfigs) { next ITEM if !$Item->{NavBar}; my $InheritPermissions = 0; # Inherit permissions from frontend registration if no permissions were defined for the navigation entry. if ( !$Item->{GroupRo} && !$Item->{Group} ) { if ( $FrontendModule->{GroupRo} ) { $Item->{GroupRo} = $FrontendModule->{GroupRo}; } if ( $FrontendModule->{Group} ) { $Item->{Group} = $FrontendModule->{Group}; } $InheritPermissions = 1; } my $Shown = 0; PERMISSION: for my $Permission (qw(GroupRo Group)) { # No access restriction. if ( ref $Item->{GroupRo} eq 'ARRAY' && !scalar @{ $Item->{GroupRo} } && ref $Item->{Group} eq 'ARRAY' && !scalar @{ $Item->{Group} } ) { $Shown = 1; last PERMISSION; } # Array access restriction. elsif ( $Item->{$Permission} && ref $Item->{$Permission} eq 'ARRAY' ) { for my $Group ( @{ $Item->{$Permission} } ) { my $HasPermission = $GroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $Group, Type => $Permission eq 'GroupRo' ? 'ro' : 'rw', ); if ($HasPermission) { $Shown = 1; last PERMISSION; } } } } # If we passed the initial permission check and didn't inherit permissions from the module registration, # make sure to also check access to the module, since navigation item might be out of sync. if ( $Shown && !$InheritPermissions ) { my $ModulePermission; PERMISSION: for my $Permission (qw(GroupRo Group)) { # No access restriction. if ( ref $FrontendModule->{$Module}->{GroupRo} eq 'ARRAY' && !scalar @{ $FrontendModule->{$Module}->{GroupRo} } && ref $FrontendModule->{$Module}->{Group} eq 'ARRAY' && !scalar @{ $FrontendModule->{$Module}->{Group} } ) { $ModulePermission = 1; last PERMISSION; } # Array access restriction. elsif ( $FrontendModule->{$Module}->{$Permission} && ref $FrontendModule->{$Module}->{$Permission} eq 'ARRAY' ) { GROUP: for my $Group ( @{ $FrontendModule->{$Module}->{$Permission} } ) { next GROUP if !$Group; my $HasPermission = $GroupObject->PermissionCheck( UserID => $Self->{UserID}, GroupName => $Group, Type => $Permission eq 'GroupRo' ? 'ro' : 'rw', ); if ($HasPermission) { $ModulePermission = 1; last PERMISSION; } } } } # Hide item if no permission was granted to access the module. if ( !$ModulePermission ) { $Shown = 0; } } next ITEM if !$Shown; # set prio of item my $Key = ( $Item->{Block} || '' ) . sprintf( "%07d", $Item->{Prio} ); COUNT: for ( 1 .. 51 ) { last COUNT if !$NavBarModule{$Key}; $Item->{Prio}++; $Key = ( $Item->{Block} || '' ) . sprintf( "%07d", $Item->{Prio} ); } # Show as main menu. if ( $Item->{Type} eq 'Menu' ) { $NavBarModule{$Key} = $Item; } # show as sub of main menu elsif ( $Item->{Type} eq 'Submenu' ) { $NavBarModule{Sub}->{ $Item->{NavBar} }->{$Key} = $Item; } } } my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); # run menu item modules if ( ref $ConfigObject->Get('CustomerFrontend::NavBarModule') eq 'HASH' ) { my %Jobs = %{ $ConfigObject->Get('CustomerFrontend::NavBarModule') }; for my $Job ( sort keys %Jobs ) { # load module if ( !$MainObject->Require( $Jobs{$Job}->{Module} ) ) { $Self->FatalError(); } my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, UserID => $Self->{UserID}, Debug => $Self->{Debug}, ); # run module %NavBarModule = ( %NavBarModule, $Object->Run( %Param, Config => $Jobs{$Job}, NavBarModule => \%NavBarModule || {}, ), ); } } my $Total = keys %NavBarModule; my $Counter = 0; if ( $NavBarModule{Sub} ) { $Total = int($Total) - 1; } # Only highlight the first matched navigation entry. If there are several entries # with the same Action and Subaction, it cannot be determined which one was used. # Therefore we just highlight the first one. my $SelectedFlag; ITEM: for my $Item ( sort keys %NavBarModule ) { next ITEM if !%{ $NavBarModule{$Item} }; next ITEM if $Item eq 'Sub'; $Counter++; my $Sub; if ( $NavBarModule{$Item}->{NavBar} ) { $Sub = $NavBarModule{Sub}->{ $NavBarModule{$Item}->{NavBar} }; } # highlight active link $NavBarModule{$Item}->{Class} = ''; if ( $NavBarModule{$Item}->{Link} ) { if ( !$SelectedFlag && $NavBarModule{$Item}->{Link} =~ /Action=$Self->{Action}/ && $NavBarModule{$Item}->{Link} =~ /$Self->{Subaction}/ # Subaction can be empty ) { $NavBarModule{$Item}->{Class} .= ' Selected'; $SelectedFlag = 1; } } if ( $Counter == $Total ) { $NavBarModule{$Item}->{Class} .= ' Last'; } $Self->Block( Name => $NavBarModule{$Item}->{Block} || 'Item', Data => $NavBarModule{$Item}, ); # show sub menu next ITEM if !$Sub; $Self->Block( Name => 'ItemAreaSub', Data => $Item, ); for my $Key ( sort keys %{$Sub} ) { my $ItemSub = $Sub->{$Key}; $ItemSub->{NameForID} = $ItemSub->{Name}; $ItemSub->{NameForID} =~ s/[ &;]//ig; $ItemSub->{NameTop} = $NavBarModule{$Item}->{NameForID}; # check if we must mark the parent element as selected if ( $ItemSub->{Link} ) { if ( $ItemSub->{Link} =~ /Action=$Self->{Action}/ && $ItemSub->{Link} =~ /$Self->{Subaction}/ # Subaction can be empty ) { $NavBarModule{$Item}->{Class} .= ' Selected'; $ItemSub->{Class} .= ' SubSelected'; $SelectedFlag = 1; } } $Self->Block( Name => 'ItemAreaSubItem', Data => { %$ItemSub, AccessKeyReference => $ItemSub->{AccessKey} ? " ($ItemSub->{AccessKey})" : '', }, ); } } # run notification modules my $FrontendNotifyModuleConfig = $ConfigObject->Get('CustomerFrontend::NotifyModule'); if ( ref $FrontendNotifyModuleConfig eq 'HASH' ) { my %Jobs = %{$FrontendNotifyModuleConfig}; NOTIFICATIONMODULE: for my $Job ( sort keys %Jobs ) { # load module next NOTIFICATIONMODULE if !$MainObject->Require( $Jobs{$Job}->{Module} ); my $Object = $Jobs{$Job}->{Module}->new( %{$Self}, LayoutObject => $Self, ); next NOTIFICATIONMODULE if !$Object; # run module $Param{Notification} .= $Object->Run( %Param, Config => $Jobs{$Job} ); } } # create the customer user login info (usually at the right side of the navigation bar) if ( !$Self->{UserLoginIdentifier} ) { $Param{UserLoginIdentifier} = $Self->{UserEmail} ne $Self->{UserCustomerID} ? "( $Self->{UserEmail} / $Self->{UserCustomerID} )" : $Self->{UserEmail}; } else { $Param{UserLoginIdentifier} = $Self->{UserLoginIdentifier}; } # only on valid session if ( $Self->{UserID} ) { # show logout button (if registered) if ( $FrontendModule->{Logout} ) { $Self->Block( Name => 'Logout', Data => \%Param, ); } # show preferences button (if registered) if ( $FrontendModule->{CustomerPreferences} ) { if ( $Self->{Action} eq 'CustomerPreferences' ) { $Param{Class} = 'Selected'; } $Self->Block( Name => 'Preferences', Data => \%Param, ); } # Show open chat requests (if chat engine is active). if ( $Kernel::OM->Get('Kernel::System::Main')->Require( 'Kernel::System::Chat', Silent => 1 ) ) { if ( $ConfigObject->Get('ChatEngine::Active') ) { my $ChatObject = $Kernel::OM->Get('Kernel::System::Chat'); my $Chats = $ChatObject->ChatList( Status => 'request', TargetType => 'Customer', ChatterID => $Self->{UserID}, ChatterType => 'Customer', ChatterActive => 0, ); my $Count = scalar $Chats; $Self->Block( Name => 'ChatRequests', Data => { Count => $Count, Class => ($Count) ? '' : 'Hidden', }, ); $Self->AddJSData( Key => 'ChatEngine::Active', Value => $ConfigObject->Get('ChatEngine::Active') ); } } } # create & return output return $Self->Output( TemplateFile => 'CustomerNavigationBar', Data => \%Param ); } sub CustomerError { my ( $Self, %Param ) = @_; # get backend error messages for (qw(Message Traceback)) { $Param{ 'Backend' . $_ } = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => $_ ) || ''; } if ( !$Param{BackendMessage} && !$Param{BackendTraceback} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Param{Message} || '?', ); for (qw(Message Traceback)) { $Param{ 'Backend' . $_ } = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => $_ ) || ''; } } if ( !$Param{Message} ) { $Param{Message} = $Param{BackendMessage}; } # create & return output return $Self->Output( TemplateFile => 'CustomerError', Data => \%Param ); } sub CustomerErrorScreen { my ( $Self, %Param ) = @_; my $Output = $Self->CustomerHeader( Title => 'Error' ); $Output .= $Self->CustomerError(%Param); $Output .= $Self->CustomerFooter(); return $Output; } sub CustomerWarning { my ( $Self, %Param ) = @_; # get backend error messages $Param{BackendMessage} = $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Notice', What => 'Message', ) || $Kernel::OM->Get('Kernel::System::Log')->GetLogEntry( Type => 'Error', What => 'Message', ) || ''; if ( !$Param{Message} ) { $Param{Message} = $Param{BackendMessage}; } # create & return output return $Self->Output( TemplateFile => 'CustomerWarning', Data => \%Param ); } sub CustomerNoPermission { my ( $Self, %Param ) = @_; my $WithHeader = $Param{WithHeader} || 'yes'; $Param{Message} ||= Translatable('No Permission!'); # create output my $Output; $Output = $Self->CustomerHeader( Title => Translatable('No Permission') ) if ( $WithHeader eq 'yes' ); $Output .= $Self->Output( TemplateFile => 'NoPermission', Data => \%Param ); $Output .= $Self->CustomerFooter() if ( $WithHeader eq 'yes' ); # return output return $Output; } =head2 Ascii2RichText() converts text to rich text my $HTMLString = $LayoutObject->Ascii2RichText( String => $TextString, ); =cut sub Ascii2RichText { my ( $Self, %Param ) = @_; # check needed stuff for (qw(String)) { if ( !defined $Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); return; } } # ascii 2 html $Param{String} = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToHTML( String => $Param{String}, ); return $Param{String}; } =head2 RichText2Ascii() converts text to rich text my $TextString = $LayoutObject->RichText2Ascii( String => $HTMLString, ); =cut sub RichText2Ascii { my ( $Self, %Param ) = @_; # check needed stuff for (qw(String)) { if ( !defined $Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); return; } } # ascii 2 html $Param{String} = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToAscii( String => $Param{String}, ); return $Param{String}; } =head2 RichTextDocumentComplete() 1) add html, body, ... tags to be a valid html document 2) replace links of inline content e. g. images to $HTMLBody = $LayoutObject->RichTextDocumentComplete( String => $HTMLBody, ); =cut sub RichTextDocumentComplete { my ( $Self, %Param ) = @_; # check needed stuff for (qw(String)) { if ( !defined $Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); return; } } # replace image link with content id for uploaded images my $StringRef = $Self->_RichTextReplaceLinkOfInlineContent( String => \$Param{String}, ); # verify html document $Param{String} = $Kernel::OM->Get('Kernel::System::HTMLUtils')->DocumentComplete( String => ${$StringRef}, Charset => $Self->{UserCharset}, ); # do correct direction if ( $Self->{TextDirection} ) { $Param{String} =~ s/HTMLLinkQuote( String => $Param{String} ); return $Param{String}; } =begin Internal: =cut =head2 _RichTextReplaceLinkOfInlineContent() replace links of inline content e. g. images $HTMLBodyStringRef = $LayoutObject->_RichTextReplaceLinkOfInlineContent( String => $HTMLBodyStringRef, ); =cut sub _RichTextReplaceLinkOfInlineContent { my ( $Self, %Param ) = @_; # check needed stuff for (qw(String)) { if ( !$Param{$_} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $_!" ); return; } } # replace image link with content id for uploaded images ${ $Param{String} } =~ s{ (]+?ContentID=(.+?)("|')([^>]*>) } { my ($Start, $CID, $Close, $End) = ($1, $3, $4, $5); # Make sure we only get the CID and not extra stuff like session information $CID =~ s{^([^;&]+).*}{$1}smx; $Start . 'cid:' . $CID . $Close . $End; }esgxi; return $Param{String}; } =end Internal: =head2 RichTextDocumentServe() Serve a rich text (HTML) document for local view inside of an C