OTRS has a large number of so-called "module layers" which make it very easy to extend the system without patching existing code. One example is the number generation mechanism for tickets. It is a "module layer" with pluggable modules, and you can add your own custom number generator modules if you wish to do so. Let's look at the different layers in detail!
There are several agent authentication modules (DB, LDAP and HTTPBasicAuth) which come with the OTRS framework. It is also possible to develop your own authentication modules. The agent authentication modules are located under Kernel/System/Auth/*.pm. For more information about their configuration see the admin manual. Following, there is an example of a simple agent auth module. Save it under Kernel/System/Auth/Simple.pm. You just need 3 functions: new(), GetOption() and Auth(). Return the uid, then the authentication is ok.
The interface class is called Kernel::System::Auth. The example agent authentication may be called Kernel::System::Auth::CustomAuth. You can find an example below.
# -- # Kernel/System/Auth/CustomAuth.pm - provides the CustomAuth authentication # based on Martin Edenhofer's Kernel::System::Auth::DB # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # ID: CustomAuth.pm,v 1.1 2010/05/10 15:30:34 fk Exp $ # -- # 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::System::Auth::CustomAuth; use strict; use warnings; use Authen::CustomAuth; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(LogObject ConfigObject DBObject)) { $Self->{$_} = $Param{$_} || die "No $_!"; } # Debug 0=off 1=on $Self->{Debug} = 0; # get config $Self->{Die} = $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Die' . $Param{Count} ); # get user table $Self->{CustomAuthHost} = $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Host' . $Param{Count} ) || die "Need AuthModule::CustomAuth::Host$Param{Count}."; $Self->{CustomAuthSecret} = $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Password' . $Param{Count} ) || die "Need AuthModule::CustomAuth::Password$Param{Count}."; return $Self; } sub GetOption { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{What} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need What!" ); return; } # module options my %Option = ( PreAuth => 0, ); # return option return $Option{ $Param{What} }; } sub Auth { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{User} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need User!" ); return; } # get params my $User = $Param{User} || ''; my $Pw = $Param{Pw} || ''; my $RemoteAddr = $ENV{REMOTE_ADDR} || 'Got no REMOTE_ADDR env!'; my $UserID = ''; my $GetPw = ''; # just in case for debug! if ( $Self->{Debug} > 0 ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: '$User' tried to authenticate with Pw: '$Pw' ($RemoteAddr)", ); } # just a note if ( !$User ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "No User given!!! (REMOTE_ADDR: $RemoteAddr)", ); return; } # just a note if ( !$Pw ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User authentication without Pw!!! (REMOTE_ADDR: $RemoteAddr)", ); return; } # Create a radius object my $CustomAuth = Authen::CustomAuth->new( Host => $Self->{CustomAuthHost}, Secret => $Self->{CustomAuthecret}, ); if ( !$CustomAuth ) { if ( $Self->{Die} ) { die "Can't connect to $Self->{CustomAuthHost}: $@"; } else { $Self->{LogObject}->Log( Priority => 'error', Message => "Can't connect to $Self->{CustomAuthHost}: $@", ); return; } } my $AuthResult = $CustomAuth->check_pwd( $User, $Pw ); # login note if ( defined($AuthResult) && $AuthResult == 1 ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User authentication ok (REMOTE_ADDR: $RemoteAddr).", ); return $User; } # just a note else { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User authentication with wrong Pw!!! (REMOTE_ADDR: $RemoteAddr)" ); return; } } 1;
There is the need to activate your custom agent authenticate module. This can be done using the perl configuration below. It is not recommended to use the xml configuration because you can lock you out via the sysconfig.
$Self->{'AuthModule'} = 'Kernel::System::Auth::CustomAuth';
There is a LDAP authentication synchronisation module which come with the OTRS framework. It is also possible to develop your own authentication modules. The authentication synchronisation modules are located under Kernel/System/Auth/Sync/*.pm. For more information about their configuration see the admin manual. Following, there is an example of an authentication synchronisation module. Save it under Kernel/System/Auth/Sync/CustomAuthSync.pm. You just need 2 functions: new() and Sync(). Return 1, then the synchronisation is ok.
The interface class is called Kernel::System::Auth. The example agent authentication may be called Kernel::System::Auth::Sync::CustomAuthSync. You can find an example below.
# -- # Kernel/System/Auth/Sync/CustomAuthSync.pm - provides the CustomAuthSync # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: CustomAuthSync.pm,v 1.9 2010/03/25 14:42:45 martin Exp $ # -- # 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::System::Auth::Sync::CustomAuthSync; use strict; use warnings; use Net::LDAP; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(LogObject ConfigObject DBObject UserObject GroupObject EncodeObject)) { $Self->{$_} = $Param{$_} || die "No $_!"; } # Debug 0=off 1=on $Self->{Debug} = 0; ... return $Self; } sub Sync { my ( $Self, %Param ) = @_; # check needed stuff for (qw(User)) { if ( !$Param{$_} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } ... return 1; }
You should activate your custom synchronisation module. This can be done using the perl configuration below. It is not recommended to use the xml configuration because this would allow you to lock yourself out via SysConfig.
$Self->{'AuthSyncModule'} = 'Kernel::System::Auth::Sync::LDAP';
There are several customer authentication modules (DB, LDAP and HTTPBasicAuth) which come with the OTRS framework. It is also possible to develop your own authentication modules. The customer authentication modules are located under Kernel/System/CustomerAuth/*.pm. For more information about their configuration see the admin manual. Following, there is an example of a simple customer auth module. Save it under Kernel/System/CustomerAuth/Simple.pm. You just need 3 functions: new(), GetOption() and Auth(). Return the uid, then the authentication is ok.
The interface class is called Kernel::System::CustomerAuth. The example customer authentication may be called Kernel::System::CustomerAuth::CustomAuth. You can find an example below.
# -- # Kernel/System/CustomerAuth/CustomAuth.pm - provides the custom Authentication # based on Martin Edenhofer's Kernel::System::Auth::DB # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: CustomAuth.pm,v 1.11 2009/09/22 15:16:05 mb Exp $ # -- # 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::System::CustomerAuth::CustomAuth; use strict; use warnings; use Authen::CustomAuth; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(LogObject ConfigObject DBObject)) { $Self->{$_} = $Param{$_} || die "No $_!"; } # Debug 0=off 1=on $Self->{Debug} = 0; # get config $Self->{Die} = $Self->{ConfigObject}->Get( 'Customer::AuthModule::CustomAuth::Die' . $Param{Count} ); # get user table $Self->{CustomAuthHost} = $Self->{ConfigObject}->Get( 'Customer::AuthModule::CustomAuth::Host' . $Param{Count} ) || die "Need Customer::AuthModule::CustomAuth::Host$Param{Count} in Kernel/Config.pm"; $Self->{CustomAuthSecret} = $Self->{ConfigObject}->Get( 'Customer::AuthModule::CustomAuth::Password' . $Param{Count} ) || die "Need Customer::AuthModule::CustomAuth::Password$Param{Count} in Kernel/Config.pm"; return $Self; } sub GetOption { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{What} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need What!" ); return; } # module options my %Option = ( PreAuth => 0, ); # return option return $Option{ $Param{What} }; } sub Auth { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{User} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need User!" ); return; } # get params my $User = $Param{User} || ''; my $Pw = $Param{Pw} || ''; my $RemoteAddr = $ENV{REMOTE_ADDR} || 'Got no REMOTE_ADDR env!'; my $UserID = ''; my $GetPw = ''; # just in case for debug! if ( $Self->{Debug} > 0 ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: '$User' tried to authentificate with Pw: '$Pw' ($RemoteAddr)", ); } # just a note if ( !$User ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "No User given!!! (REMOTE_ADDR: $RemoteAddr)", ); return; } # just a note if ( !$Pw ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User Authentication without Pw!!! (REMOTE_ADDR: $RemoteAddr)", ); return; } # Create a custom object my $CustomAuth = Authen::CustomAuth->new( Host => $Self->{CustomAuthHost}, Secret => $Self->{CustomAuthSecret}, ); if ( !$CustomAuth ) { if ( $Self->{Die} ) { die "Can't connect to $Self->{CustomAuthHost}: $@"; } else { $Self->{LogObject}->Log( Priority => 'error', Message => "Can't connect to $Self->{CustomAuthHost}: $@", ); return; } } my $AuthResult = $CustomAuth->check_pwd( $User, $Pw ); # login note if ( defined($AuthResult) && $AuthResult == 1 ) { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User Authentication ok (REMOTE_ADDR: $RemoteAddr).", ); return $User; } # just a note else { $Self->{LogObject}->Log( Priority => 'notice', Message => "User: $User Authentication with wrong Pw!!! (REMOTE_ADDR: $RemoteAddr)" ); return; } } 1;
There is the need to activate your custom customer authenticate module. This can be done using the xml configuration below.
<ConfigItem Name="AuthModule" Required="1" Valid="1"> <Description Lang="en">Module to authenticate customers.</Description> <Description Lang="de">Modul zum Authentifizieren der Customer.</Description> <Group>Framework</Group> <SubGroup>Frontend::CustomerAuthAuth</SubGroup> <Setting> <Option Location="Kernel/System/CustomerAuth/*.pm" SelectedID="Kernel::System::CustomerAuth::CustomAuth"></Option> </Setting> </ConfigItem>
There is a DB customer-user preferences module which come with the OTRS framework. It is also possible to develop your own customer-user preferences modules. The customer-user preferences modules are located under Kernel/System/CustomerUser/Preferences/*.pm. For more information about their configuration see the admin manual. There is an example of a customer-user preferences module below. Save it under Kernel/System/CustomerUser/Preferences/Custom.pm. You just need 4 functions: new(), SearchPreferences(), SetPreferences() and GetPreferences().
The interface class is called Kernel::System::CustomerUser. The example customer-user preferences may be called Kernel::System::CustomerUser::Preferences::Custom. You can find an example below.
# -- # Kernel/System/CustomerUser/Preferences/Custom.pm - some customer user functions # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: Custom.pm,v 1.20 2009/10/07 20:41:50 martin Exp $ # -- # 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::System::CustomerUser::Preferences::Custom; use strict; use warnings; use vars qw(@ISA $VERSION); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Object (qw(DBObject ConfigObject LogObject)) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } # preferences table data $Self->{PreferencesTable} = $Self->{ConfigObject}->Get('CustomerPreferences')->{Params}->{Table} || 'customer_preferences'; $Self->{PreferencesTableKey} = $Self->{ConfigObject}->Get('CustomerPreferences')->{Params}->{TableKey} || 'preferences_key'; $Self->{PreferencesTableValue} = $Self->{ConfigObject}->Get('CustomerPreferences')->{Params}->{TableValue} || 'preferences_value'; $Self->{PreferencesTableUserID} = $Self->{ConfigObject}->Get('CustomerPreferences')->{Params}->{TableUserID} || 'user_id'; return $Self; } sub SetPreferences { my ( $Self, %Param ) = @_; my $UserID = $Param{UserID} || return; my $Key = $Param{Key} || return; my $Value = defined( $Param{Value} ) ? $Param{Value} : ''; # delete old data return if !$Self->{DBObject}->Do( SQL => "DELETE FROM $Self->{PreferencesTable} WHERE " . " $Self->{PreferencesTableUserID} = ? AND $Self->{PreferencesTableKey} = ?", Bind => [ \$UserID, \$Key ], ); $Value .= 'Custom'; # insert new data return if !$Self->{DBObject}->Do( SQL => "INSERT INTO $Self->{PreferencesTable} ($Self->{PreferencesTableUserID}, " . " $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue}) " . " VALUES (?, ?, ?)", Bind => [ \$UserID, \$Key, \$Value ], ); return 1; } sub GetPreferences { my ( $Self, %Param ) = @_; my $UserID = $Param{UserID} || return; my %Data; # get preferences return if !$Self->{DBObject}->Prepare( SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} " . " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableUserID} = ?", Bind => [ \$UserID ], ); while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # return data return %Data; } sub SearchPreferences { my ( $Self, %Param ) = @_; my %UserID; my $Key = $Param{Key} || ''; my $Value = $Param{Value} || ''; # get preferences my $SQL = "SELECT $Self->{PreferencesTableUserID}, $Self->{PreferencesTableValue} " . " FROM " . " $Self->{PreferencesTable} " . " WHERE " . " $Self->{PreferencesTableKey} = '" . $Self->{DBObject}->Quote($Key) . "'" . " AND " . " LOWER($Self->{PreferencesTableValue}) LIKE LOWER('" . $Self->{DBObject}->Quote( $Value, 'Like' ) . "')"; return if !$Self->{DBObject}->Prepare( SQL => $SQL ); while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $UserID{ $Row[0] } = $Row[1]; } # return data return %UserID; } 1;
There is the need to activate your custom customer-user preferences module. This can be done using the xml configuration below.
<ConfigItem Name="CustomerPreferences" Required="1" Valid="1"> <Description Lang="en">Parameters for the customer preference table.</Description> <Description Lang="de">Parameter für die Tabelle mit den Einstellungen für die Customer.</Description> <Group>Framework</Group> <SubGroup>Frontend::Customer::Preferences</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::System::CustomerUser::Preferences::Custom</Item> <Item Key="Params"> <Hash> <Item Key="Table">customer_preferences</Item> <Item Key="TableKey">preferences_key</Item> <Item Key="TableValue">preferences_value</Item> <Item Key="TableUserID">user_id</Item> </Hash> </Item> </Hash> </Setting> </ConfigItem>
There is a DB queue preferences module which come with the OTRS framework. It is also possible to develop your own queue preferences modules. The queue preferences modules are located under Kernel/System/Queue/*.pm. For more information about their configuration see the admin manual. Following, there is an example of a queue preferences module. Save it under Kernel/System/Queue/PreferencesCustom.pm. You just need 3 functions: new(), QueuePreferencesSet() and QueuePreferencesGet(). Return 1, then the synchronisation is ok.
The interface class is called Kernel::System::Queue. The example queue preferences may be called Kernel::System::Queue::PreferencesCustom. You can find an example below.
# -- # Kernel/System/Queue/PreferencesCustom.pm - some user functions # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: PreferencesCustom.pm,v 1.5 2009/02/16 11:47:34 tr Exp $ # -- # 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::System::Queue::PreferencesCustom; use strict; use warnings; use vars qw(@ISA $VERSION); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(DBObject ConfigObject LogObject)) { $Self->{$_} = $Param{$_} || die "Got no $_!"; } # preferences table data $Self->{PreferencesTable} = 'queue_preferences'; $Self->{PreferencesTableKey} = 'preferences_key'; $Self->{PreferencesTableValue} = 'preferences_value'; $Self->{PreferencesTableQueueID} = 'queue_id'; return $Self; } sub QueuePreferencesSet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(QueueID Key Value)) { if ( !defined( $Param{$_} ) ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # delete old data return if !$Self->{DBObject}->Do( SQL => "DELETE FROM $Self->{PreferencesTable} WHERE " . "$Self->{PreferencesTableQueueID} = ? AND $Self->{PreferencesTableKey} = ?", Bind => [ \$Param{QueueID}, \$Param{Key} ], ); $Self->{PreferencesTableValue} .= 'PreferencesCustom'; # insert new data return $Self->{DBObject}->Do( SQL => "INSERT INTO $Self->{PreferencesTable} ($Self->{PreferencesTableQueueID}, " . " $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue}) " . " VALUES (?, ?, ?)", Bind => [ \$Param{QueueID}, \$Param{Key}, \$Param{Value} ], ); } sub QueuePreferencesGet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(QueueID)) { if ( !$Param{$_} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # check if queue preferences are available if ( !$Self->{ConfigObject}->Get('QueuePreferences') ) { return; } # get preferences return if !$Self->{DBObject}->Prepare( SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} " . " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableQueueID} = ?", Bind => [ \$Param{QueueID} ], ); my %Data; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # return data return %Data; } 1;
There is the need to activate your custom queue preferences module. This can be done using the xml configuration below.
<ConfigItem Name="Queue::PreferencesModule" Required="1" Valid="1"> <Description Lang="en">Default queue preferences module.</Description> <Description Lang="de">Standard Queue Preferences Module.</Description> <Group>Ticket</Group> <SubGroup>Frontend::Queue::Preferences</SubGroup> <Setting> <String Regex="">Kernel::System::Queue::PreferencesCustom</String> </Setting> </ConfigItem>
There is a DB service preferences module which come with the OTRS framework. It is also possible to develop your own service preferences modules. The service preferences modules are located under Kernel/System/Service/*.pm. For more information about their configuration see the admin manual. Following, there is an example of a service preferences module. Save it under Kernel/System/Service/PreferencesCustom.pm. You just need 3 functions: new(), ServicePreferencesSet() and ServicePreferencesGet(). Return 1, then the synchronisation is ok.
The interface class is called Kernel::System::Service. The example service preferences may be called Kernel::System::Service::PreferencesCustom. You can find an example below.
# -- # Kernel/System/Service/PreferencesCustom - some user functions # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: PreferencesCustom.pm,v 1.2 2009/02/16 11:47:34 tr Exp $ # -- # 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::System::Service::PreferencesCustom; use strict; use warnings; use vars qw(@ISA $VERSION); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(DBObject ConfigObject LogObject)) { $Self->{$_} = $Param{$_} || die "Got no $_!"; } # preferences table data $Self->{PreferencesTable} = 'service_preferences'; $Self->{PreferencesTableKey} = 'preferences_key'; $Self->{PreferencesTableValue} = 'preferences_value'; $Self->{PreferencesTableServiceID} = 'service_id'; return $Self; } sub ServicePreferencesSet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(ServiceID Key Value)) { if ( !defined( $Param{$_} ) ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # delete old data return if !$Self->{DBObject}->Do( SQL => "DELETE FROM $Self->{PreferencesTable} WHERE " . "$Self->{PreferencesTableServiceID} = ? AND $Self->{PreferencesTableKey} = ?", Bind => [ \$Param{ServiceID}, \$Param{Key} ], ); $Self->{PreferencesTableValue} .= 'PreferencesCustom'; # insert new data return $Self->{DBObject}->Do( SQL => "INSERT INTO $Self->{PreferencesTable} ($Self->{PreferencesTableServiceID}, " . " $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue}) " . " VALUES (?, ?, ?)", Bind => [ \$Param{ServiceID}, \$Param{Key}, \$Param{Value} ], ); } sub ServicePreferencesGet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(ServiceID)) { if ( !$Param{$_} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # check if service preferences are available if ( !$Self->{ConfigObject}->Get('ServicePreferences') ) { return; } # get preferences return if !$Self->{DBObject}->Prepare( SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} " . " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableServiceID} = ?", Bind => [ \$Param{ServiceID} ], ); my %Data; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # return data return %Data; } 1;
There is the need to activate your custom service preferences module. This can be done using the xml configuration below.
<ConfigItem Name="Service::PreferencesModule" Required="1" Valid="1"> <Description Lang="en">Default service preferences module.</Description> <Description Lang="de">Standard Service Preferences Module.</Description> <Group>Ticket</Group> <SubGroup>Frontend::Service::Preferences</SubGroup> <Setting> <String Regex="">Kernel::System::Service::PreferencesCustom</String> </Setting> </ConfigItem>
There is a DB SLA preferences module which come with the OTRS framework. It is also possible to develop your own SLA preferences modules. The SLA preferences modules are located under Kernel/System/SLA/*.pm. For more information about their configuration see the admin manual. Here we'll show an example of an SLA preferences module. Save it under Kernel/System/SLA/PreferencesCustom.pm. You just need 3 functions: new(), SLAPreferencesSet() and SLAPreferencesGet(). Make sure the function returns 1.
The interface class is called Kernel::System::SLA. The example SLA preferences may be called Kernel::System::SLA::PreferencesCustom. You can find an example below.
# -- # Kernel/System/SLA/PreferencesCustom.pm - some user functions # Copyright (C) 2001-2018 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::System::SLA::PreferencesCustom; use strict; use warnings; use vars qw(@ISA); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for (qw(DBObject ConfigObject LogObject)) { $Self->{$_} = $Param{$_} || die "Got no $_!"; } # preferences table data $Self->{PreferencesTable} = 'sla_preferences'; $Self->{PreferencesTableKey} = 'preferences_key'; $Self->{PreferencesTableValue} = 'preferences_value'; $Self->{PreferencesTableSLAID} = 'sla_id'; return $Self; } sub SLAPreferencesSet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(SLAID Key Value)) { if ( !defined( $Param{$_} ) ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # delete old data return if !$Self->{DBObject}->Do( SQL => "DELETE FROM $Self->{PreferencesTable} WHERE " . "$Self->{PreferencesTableSLAID} = ? AND $Self->{PreferencesTableKey} = ?", Bind => [ \$Param{SLAID}, \$Param{Key} ], ); $Self->{PreferencesTableValue} .= 'PreferencesCustom'; # insert new data return $Self->{DBObject}->Do( SQL => "INSERT INTO $Self->{PreferencesTable} ($Self->{PreferencesTableSLAID}, " . " $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue}) " . " VALUES (?, ?, ?)", Bind => [ \$Param{SLAID}, \$Param{Key}, \$Param{Value} ], ); } sub SLAPreferencesGet { my ( $Self, %Param ) = @_; # check needed stuff for (qw(SLAID)) { if ( !$Param{$_} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" ); return; } } # check if service preferences are available if ( !$Self->{ConfigObject}->Get('SLAPreferences') ) { return; } # get preferences return if !$Self->{DBObject}->Prepare( SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} " . " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableSLAID} = ?", Bind => [ \$Param{SLAID} ], ); my %Data; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # return data return %Data; } 1;
You need to register your custom SLA preferences module. This can be done using the xml configuration below.
<ConfigItem Name="SLA::PreferencesModule" Required="1" Valid="1"> <Description Translatable="1">Default SLA preferences module.</Description> <Group>Ticket</Group> <SubGroup>Frontend::SLA::Preferences</SubGroup> <Setting> <String Regex="">Kernel::System::SLA::PreferencesCustom</String> </Setting> </ConfigItem>
There is a gobal log interface for OTRS that provides the possibility to create own log backends.
Writing an own logging backend is as easy as reimplementing the Kernel::System::Log::Log() method.
In this small example, we'll write a little file logging backend which works similar to Kernel::System::Log::File, but prepends a string to each logging entry.
# -- # Kernel/System/Log/CustomFile.pm - file log backend # Copyright (C) 2001-2018 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::System::Log::CustomFile; use strict; use warnings; umask "002"; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # get needed objects for (qw(ConfigObject EncodeObject)) { if ( $Param{$_} ) { $Self->{$_} = $Param{$_}; } else { die "Got no $_!"; } } # get logfile location $Self->{LogFile} = '/var/log/CustomFile.log'; # set custom prefix $Self->{CustomPrefix} = 'CustomFileExample'; # Fixed bug# 2265 - For IIS we need to create a own error log file. # Bind stderr to log file, because iis do print stderr to web page. if ( $ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /^microsoft\-iis/i ) { if ( !open STDERR, '>>', $Self->{LogFile} . '.error' ) { print STDERR "ERROR: Can't write $Self->{LogFile}.error: $!"; } } return $Self; } sub Log { my ( $Self, %Param ) = @_; my $FH; # open logfile if ( !open $FH, '>>', $Self->{LogFile} ) { # print error screen print STDERR "\n"; print STDERR " >> Can't write $Self->{LogFile}: $! <<\n"; print STDERR "\n"; return; } # write log file $Self->{EncodeObject}->SetIO($FH); print $FH '[' . localtime() . ']'; if ( lc $Param{Priority} eq 'debug' ) { print $FH "[Debug][$Param{Module}][$Param{Line}] $Self->{CustomPrefix} $Param{Message}\n"; } elsif ( lc $Param{Priority} eq 'info' ) { print $FH "[Info][$Param{Module}] $Self->{CustomPrefix} $Param{Message}\n"; } elsif ( lc $Param{Priority} eq 'notice' ) { print $FH "[Notice][$Param{Module}] $Self->{CustomPrefix} $Param{Message}\n"; } elsif ( lc $Param{Priority} eq 'error' ) { print $FH "[Error][$Param{Module}][$Param{Line}] $Self->{CustomPrefix} $Param{Message}\n"; } else { # print error messages to STDERR print STDERR "[Error][$Param{Module}] $Self->{CustomPrefix} Priority: '$Param{Priority}' not defined! Message: $Param{Message}\n"; # and of course to logfile print $FH "[Error][$Param{Module}] $Self->{CustomPrefix} Priority: '$Param{Priority}' not defined! Message: $Param{Message}\n"; } # close file handle close $FH; return 1; } 1;
To activate our custom logging module, the administrator can either set the existing configuration item "LogModule" manually to "Kernel::System::Log::CustomFile". To realize this automatically, you can provide an XML configuration file which overrides the default setting.
<ConfigItem Name="LogModule" Required="1" Valid="1"> <Description Translatable="1">Set Kernel::System::Log::CustomFile as default logging backend.</Description> <Group>Framework</Group> <SubGroup>Core::Log</SubGroup> <Setting> <Option Location="Kernel/System/Log/*.pm" SelectedID="Kernel::System::Log::CustomFile"></Option> </Setting> </ConfigItem>
Output filters allow to modify HTML on the fly. It is best practice to use output filters
instead of modifying .tt
files directly.
There are three good reasons for that.
When the same adaption has to be applied to several frontend modules then the adaption only has to be implemented once.
The second advantage is that when OTRS is upgraded
there is a chance that the filter doesn't have to be updated, when the relevant pattern has not changed.
When two extensions modify the same file there is a conflict during the installation of the second package.
This conflict can be resolved by using two output filters that modify the same frontend module.
There are four different kinds of output filters. They are active at different stages of the generation of HTML content.
The content of a template can be changed by the filter before any processing by the Layout module takes place. This kind of filter should be used in most cases. Processing instructions like [% Translate() %], [% Data.Name | html %] can be inserted into the template content and they will be honored by the subsequent template processing.
The content of a template can be changed after variable substitution and translation. The kind of filter should only be used when the filter needs access to translated strings or to substituted variables.
This kind of filter allows to process the complete HTML output for the request right before it is sent to the browser. This can be used for global transformations. But in real life there is rarely a need to use this kind of filter.
This kind of output filter is a plugin for the method Kernel::Output::HTML::Layout::Ascii2HTML() and is only active when the parameter LinkFeature is set to 1. Thus the FilterText output filters are currently only active for the display of the body of plain text articles. Plain text articles are generated by incoming non-HTML mails and when OTRS is configured to not use the rich text feature in the frontend.
All ticket attributes are passed to the AgentTicketZoom template. Therefore it suffices to insert e.g. the instruction $QData{"Title"} into the content. This can be achieved with a FilterElementPre output filter.
An additional CSS file can be added to all agent frontends with an FilterElementPre filter
that only modifies Header.tt
.
Therefore it suffices to insert e.g. the instruction $QData{"Title"} into the content.
This can be achieved with a FilterElementPre output filter.
Use a FilterElementPost for this feature. The list of selectable services can be parsed from the processed template output. The multi level selection can be constructed from the service list and inserted into the template content. A FilterElementPost output filter must be used for that.
A biotech company uses gene names like IPI00217472 in plain text articles. A FilterText output filter can be used to create links to a sequence database, e.g. http://srs.ebi.ac.uk/srsbin/cgi-bin/wgetz?-e+[IPI-acc:IPI00217472]+-vn+2, for the gene names.
Every ElementPre and ElementPost output filter is constructed and run for every Template that is needed for the current request. Thus low performance of the output filter or a large number of filters can severely degrade performance. When that becomes an issue, the construction of needed objects can be done in the Run-method after the checks. Thus the expensive code is run only in the relevant cases.
In order to increase flexibility the list of affected templates should be configurable in SysConfig.
There are two different types of internal stats modules - dynamic and static. This section describes how such stats modules can be developed.
In contrast to static stats modules, dynamic statistics can be configured via the OTRS web interface. In this section a simple statistic module is developed. Each dynamic stats module has to implement these subroutines
new
GetObjectName
GetObjectAttributes
ExportWrapper
ImportWrapper
Furthermore the module has to implement either GetStatElement or GetStatTable. And if the header line of the result table should be changed, a sub called GetHeaderLine has to be developed.
In this section a sample stats module is shown and each subroutine is explained.
# -- # Kernel/System/Stats/Dynamic/DynamicStatsTemplate.pm - all advice functions # Copyright (C) 2001-2018 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::System::Stats::Dynamic::DynamicStatsTemplate; use strict; use warnings; use Kernel::System::Queue; use Kernel::System::State; use Kernel::System::Ticket;
This is common boilerplate that can be found in common OTRS modules. The class/package name is declared via the package keyword. Then the needed modules are 'use'd.
sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Object ( qw(DBObject ConfigObject LogObject UserObject TimeObject MainObject EncodeObject) ) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } # created needed objects $Self->{QueueObject} = Kernel::System::Queue->new( %{$Self} ); $Self->{TicketObject} = Kernel::System::Ticket->new( %{$Self} ); $Self->{StateObject} = Kernel::System::State->new( %{$Self} ); return $Self; }
new is the constructor for this statistic module. It creates a new instance of the class. According to the coding guidelines objects of other classes that are needed in this module have to be created in "new". In lines 27 to 29 the object of the stats module is created. Lines 31 to 37 check if objects that are needed in this code - either for creating other objects or in this module - are passed. After that the other objects are created.
sub GetObjectName { my ( $Self, %Param ) = @_; return 'Sample Statistics'; }
GetObjectName returns a Name for the Statistics module. This is the label that is shown in the drop down in the configuration as well as in the list of existing statistics (column "object").
sub GetObjectAttributes { my ( $Self, %Param ) = @_; # get state list my %StateList = $Self->{StateObject}->StateList( UserID => 1, ); # get queue list my %QueueList = $Self->{QueueObject}->GetAllQueues(); # get current time to fix bug#3830 my $TimeStamp = $Self->{TimeObject}->CurrentTimestamp(); my ($Date) = split /\s+/, $TimeStamp; my $Today = sprintf "%s 23:59:59", $Date; my @ObjectAttributes = ( { Name => 'State', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'StateIDs', Block => 'MultiSelectField', Values => \%StateList, }, { Name => 'Created in Queue', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'CreatedQueueIDs', Block => 'MultiSelectField', Translation => 0, Values => \%QueueList, }, { Name => 'Create Time', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'CreateTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', TimeStop => $Today, Values => { TimeStart => 'TicketCreateTimeNewerDate', TimeStop => 'TicketCreateTimeOlderDate', }, }, ); return @ObjectAttributes; }
In this sample stats module, we want to provide three attributes the user can chose from: A list of queues, a list of states an a time drop down. To get the values shown in the drop down, some operations are needed. In this case call StateList and GetAllQueues.
Then the list of attributes is created. Each attribute is defined via a hashreference. You can use these keys:
Name
the label in the web interface
UseAsXvalue
Can this attribute be used on the x-axis
UseAsValueSeries
Can this attribute be used on the y-axis
UseAsRestriction
Can this attribute be used for restrictions.
Element
the HTML fieldname
Block
the block name in the template file (e.g. <OTRS_HOME>/Kernel/Output/HTML/Standard/AgentStatsEditXaxis.tt)
Values
the values shown in the attribute
Hint: If you install this sample an you configured a statistic with some queues - lets say 'queue A' and 'queue B' - then these queues are the only ones that are shown to the user when he starts the statistic. Sometimes a dynamic drop down or multiselect field is needed. In this case, you can set "SelectedValues" in the definition of the attribute:
{ Name => 'Created in Queue', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'CreatedQueueIDs', Block => 'MultiSelectField', Translation => 0, Values => \%QueueList, SelectedValues => [ @SelectedQueues ], },
sub GetStatElement { my ( $Self, %Param ) = @_; # search tickets return $Self->{TicketObject}->TicketSearch( UserID => 1, Result => 'COUNT', Permission => 'ro', Limit => 100_000_000, %Param, ); }
GetStatElement gets called for each cell in the result table. So it should be a numeric value. In this sample it does a simple ticket search. The hash %Param contains information about the "current" x-value and the y-value as well as any restrictions. So, for a cell that should count the created tickets for queue 'Misc' with state 'open' the passed parameter hash looks something like this:
'CreatedQueueIDs' => [ '4' ], 'StateIDs' => [ '2' ]
If the "per cell" calculation should be avoided, GetStatTable is an alternative. GetStatTable returns a list of rows, hence an array of arrayreferences. This leads to the same result as using GetStatElement
sub GetStatTable { my ( $Self, %Param ) = @_; my @StatData; for my $StateName ( keys %{ $Param{TableStructure} } ) { my @Row; for my $Params ( @{ $Param{TableStructure}->{$StateName} } ) { my $Tickets = $Self->{TicketObject}->TicketSearch( UserID => 1, Result => 'COUNT', Permission => 'ro', Limit => 100_000_000, %{$Params}, ); push @Row, $Tickets; } push @StatData, [ $StateName, @Row ]; } return @StatData; }
GetStatTable gets all information about the stats query that is needed. The passed parameters contains information about the attributes (Restrictions, attributes that are used for x/y-axis) and the table structure. The table structure is a hash reference where the keys are the values of the y-axis and their values are hashreferences with the parameters used for GetStatElement subroutines.
'Restrictions' => {}, 'TableStructure' => { 'closed successful' => [ { 'CreatedQueueIDs' => [ '3' ], 'StateIDs' => [ '2' ] }, ], 'closed unsuccessful' => [ { 'CreatedQueueIDs' => [ '3' ], 'StateIDs' => [ '3' ] }, ], }, 'ValueSeries' => [ { 'Block' => 'MultiSelectField', 'Element' => 'StateIDs', 'Name' => 'State', 'SelectedValues' => [ '5', '3', '2', '1', '4' ], 'Translation' => 1, 'Values' => { '1' => 'new', '10' => 'closed with workaround', '2' => 'closed successful', '3' => 'closed unsuccessful', '4' => 'open', '5' => 'removed', '6' => 'pending reminder', '7' => 'pending auto close+', '8' => 'pending auto close-', '9' => 'merged' } } ], 'XValue' => { 'Block' => 'MultiSelectField', 'Element' => 'CreatedQueueIDs', 'Name' => 'Created in Queue', 'SelectedValues' => [ '3', '4', '1', '2' ], 'Translation' => 0, 'Values' => { '1' => 'Postmaster', '2' => 'Raw', '3' => 'Junk', '4' => 'Misc' } }
Sometimes the headers of the table have to be changed. In that case, a subroutine called GetHeaderLine has to be implemented. That subroutine has to return an arrayreference with the column headers as elements. It gets information about the x-values passed.
sub GetHeaderLine { my ( $Self, %Param ) = @_; my @HeaderLine = (''); for my $SelectedXValue ( @{ $Param{XValue}->{SelectedValues} } ) { push @HeaderLine, $Param{XValue}->{Values}->{$SelectedXValue}; } return \@HeaderLine; }
sub ExportWrapper { my ( $Self, %Param ) = @_; # wrap ids to used spelling for my $Use (qw(UseAsValueSeries UseAsRestriction UseAsXvalue)) { ELEMENT: for my $Element ( @{ $Param{$Use} } ) { next ELEMENT if !$Element || !$Element->{SelectedValues}; my $ElementName = $Element->{Element}; my $Values = $Element->{SelectedValues}; if ( $ElementName eq 'QueueIDs' || $ElementName eq 'CreatedQueueIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $Self->{QueueObject}->QueueLookup( QueueID => $ID->{Content} ); } } elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) { my %StateList = $Self->{StateObject}->StateList( UserID => 1 ); ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $StateList{ $ID->{Content} }; } } } } return \%Param; }
Configured statistics can be exported into XML format. But as queues with the same queue names can have different IDs on different OTRS instances it would be quite painful to export the IDs (the statistics would calculate the wrong numbers then). So an export wrapper should be written to use the names instead of ids. This should be done for each "dimension" of the stats module (x-axis, y-axis and restrictions).
ImportWrapper works the other way around - it converts the name to the ID in the instance the configuration is imported to.
This is a sample export:
<?xml version="1.0" encoding="utf-8"?> <otrs_stats> <Cache>0</Cache> <Description>Sample stats module</Description> <File></File> <Format>CSV</Format> <Format>Print</Format> <Object>DeveloperManualSample</Object> <ObjectModule>Kernel::System::Stats::Dynamic::DynamicStatsTemplate</ObjectModule> <ObjectName>Sample Statistics</ObjectName> <Permission>stats</Permission> <StatType>dynamic</StatType> <SumCol>0</SumCol> <SumRow>0</SumRow> <Title>Sample 1</Title> <UseAsValueSeries Element="StateIDs" Fixed="1"> <SelectedValues>removed</SelectedValues> <SelectedValues>closed unsuccessful</SelectedValues> <SelectedValues>closed successful</SelectedValues> <SelectedValues>new</SelectedValues> <SelectedValues>open</SelectedValues> </UseAsValueSeries> <UseAsXvalue Element="CreatedQueueIDs" Fixed="1"> <SelectedValues>Junk</SelectedValues> <SelectedValues>Misc</SelectedValues> <SelectedValues>Postmaster</SelectedValues> <SelectedValues>Raw</SelectedValues> </UseAsXvalue> <Valid>1</Valid> </otrs_stats>
Now, that all subroutines are explained, this is the complete sample stats module.
# -- # Kernel/System/Stats/Dynamic/DynamicStatsTemplate.pm - all advice functions # Copyright (C) 2001-2018 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::System::Stats::Dynamic::DynamicStatsTemplate; use strict; use warnings; use Kernel::System::Queue; use Kernel::System::State; use Kernel::System::Ticket; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Object ( qw(DBObject ConfigObject LogObject UserObject TimeObject MainObject EncodeObject) ) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } # created needed objects $Self->{QueueObject} = Kernel::System::Queue->new( %{$Self} ); $Self->{TicketObject} = Kernel::System::Ticket->new( %{$Self} ); $Self->{StateObject} = Kernel::System::State->new( %{$Self} ); return $Self; } sub GetObjectName { my ( $Self, %Param ) = @_; return 'Sample Statistics'; } sub GetObjectAttributes { my ( $Self, %Param ) = @_; # get state list my %StateList = $Self->{StateObject}->StateList( UserID => 1, ); # get queue list my %QueueList = $Self->{QueueObject}->GetAllQueues(); # get current time to fix bug#3830 my $TimeStamp = $Self->{TimeObject}->CurrentTimestamp(); my ($Date) = split /\s+/, $TimeStamp; my $Today = sprintf "%s 23:59:59", $Date; my @ObjectAttributes = ( { Name => 'State', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'StateIDs', Block => 'MultiSelectField', Values => \%StateList, }, { Name => 'Created in Queue', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'CreatedQueueIDs', Block => 'MultiSelectField', Translation => 0, Values => \%QueueList, }, { Name => 'Create Time', UseAsXvalue => 1, UseAsValueSeries => 1, UseAsRestriction => 1, Element => 'CreateTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', TimeStop => $Today, Values => { TimeStart => 'TicketCreateTimeNewerDate', TimeStop => 'TicketCreateTimeOlderDate', }, }, ); return @ObjectAttributes; } sub GetStatElement { my ( $Self, %Param ) = @_; # search tickets return $Self->{TicketObject}->TicketSearch( UserID => 1, Result => 'COUNT', Permission => 'ro', Limit => 100_000_000, %Param, ); } sub ExportWrapper { my ( $Self, %Param ) = @_; # wrap ids to used spelling for my $Use (qw(UseAsValueSeries UseAsRestriction UseAsXvalue)) { ELEMENT: for my $Element ( @{ $Param{$Use} } ) { next ELEMENT if !$Element || !$Element->{SelectedValues}; my $ElementName = $Element->{Element}; my $Values = $Element->{SelectedValues}; if ( $ElementName eq 'QueueIDs' || $ElementName eq 'CreatedQueueIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $Self->{QueueObject}->QueueLookup( QueueID => $ID->{Content} ); } } elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) { my %StateList = $Self->{StateObject}->StateList( UserID => 1 ); ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $StateList{ $ID->{Content} }; } } } } return \%Param; } sub ImportWrapper { my ( $Self, %Param ) = @_; # wrap used spelling to ids for my $Use (qw(UseAsValueSeries UseAsRestriction UseAsXvalue)) { ELEMENT: for my $Element ( @{ $Param{$Use} } ) { next ELEMENT if !$Element || !$Element->{SelectedValues}; my $ElementName = $Element->{Element}; my $Values = $Element->{SelectedValues}; if ( $ElementName eq 'QueueIDs' || $ElementName eq 'CreatedQueueIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; if ( $Self->{QueueObject}->QueueLookup( Queue => $ID->{Content} ) ) { $ID->{Content} = $Self->{QueueObject}->QueueLookup( Queue => $ID->{Content} ); } else { $Self->{LogObject}->Log( Priority => 'error', Message => "Import: Can' find the queue $ID->{Content}!" ); $ID = undef; } } } elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; my %State = $Self->{StateObject}->StateGet( Name => $ID->{Content}, Cache => 1, ); if ( $State{ID} ) { $ID->{Content} = $State{ID}; } else { $Self->{LogObject}->Log( Priority => 'error', Message => "Import: Can' find state $ID->{Content}!" ); $ID = undef; } } } } } return \%Param; } 1;
<?xml version="1.0" encoding="iso-8859-1" ?> <otrs_config version="1.0" init="Config"> <ConfigItem Name="Stats::DynamicObjectRegistration###DynamicStatsTemplate" Required="0" Valid="1"> <Description Lang="en">Here you can decide if the common stats module may generate stats about the number of default tickets a requester created.</Description> <Group>Framework</Group> <SubGroup>Core::Stats</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::System::Stats::Dynamic::DynamicStatsTemplate</Item> </Hash> </Setting> </ConfigItem> </otrs_config>
If you have a lot of cells in the result table and the GetStatElement is quite complex, the request can take a long time.
The subsequent paragraphs describe the static stats. Static stats are very easy to create as these modules have to implement only three subroutines.
new
Param
Run
The following paragraphs describe the subroutines needed in a static stats.
sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {%Param}; bless( $Self, $Type ); # check all needed objects for my $Needed ( qw(DBObject ConfigObject LogObject TimeObject MainObject EncodeObject) ) { $Self->{$Needed} = $Param{$Needed} || die "Got no $Needed"; } # create needed objects $Self->{TypeObject} = Kernel::System::Type->new( %{$Self} ); $Self->{TicketObject} = Kernel::System::Ticket->new( %{$Self} ); $Self->{QueueObject} = Kernel::System::Queue->new( %{$Self} ); return $Self; }
new creates a new instance of the static stats class. First it creates a new object and then it checks for the needed objects.
sub Param { my $Self = shift; my %Queues = $Self->{QueueObject}->GetAllQueues(); my %Types = $Self->{TypeObject}->TypeList( Valid => 1, ); my @Params = ( { Frontend => 'Type', Name => 'TypeIDs', Multiple => 1, Size => 3, Data => \%Types, }, { Frontend => 'Queue', Name => 'QueueIDs', Multiple => 1, Size => 3, Data => \%Queues, }, ); return @Params; }
The Param method provides the list of all parameters/attributes that can be selected to create a static stat. It gets some parameters passed: The values for the stats attributes provided in a request, the format of the stats and the name of the object (name of the module).
The parameters/attributes have to be hashreferences with these key-value-pairs.
Frontent
the label in the web interface
Name
the HTML fieldname
Data
the values shown in the attribute
Other parameter for the BuildSelection method of the LayoutObject can be used, as it is done with "Size" and "Multiple" in this sample module.
sub Run { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(TypeIDs QueueIDs)) { if ( !$Param{$Needed} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # set report title my $Title = 'Tickets per Queue'; # table headlines my @HeadData = ( 'Ticket Number', 'Queue', 'Type', ); my @Data; my @TicketIDs = $Self->{TicketObject}->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', %Param, ); for my $TicketID ( @TicketIDs ) { my %Ticket = $Self->{TicketObject}->TicketGet( UserID => 1, TicketID => $TicketID, ); push @Data, [ $Ticket{TicketNumber}, $Ticket{Queue}, $Ticket{Type} ]; } return ( [$Title], [@HeadData], @Data ); }
The Run method actually generates the table data for the stats. It gets the attributes for this stats passed. In this sample it in %Param a key 'TypeIDs' and a key 'QueueIDs' exist (see attributes in Param method) and their values are arrayreferences. The returned data consists of three parts: Two arrayreferences and an array. In the first arrayreference the title for the statistic is stored, the second arrayreference contains the headlines for the columns in the table. And then the data for the table body follow.
# -- # Kernel/System/Stats/Static/StaticStatsTemplate.pm # Copyright (C) 2001-2018 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::System::Stats::Static::StaticStatsTemplate; use strict; use warnings; use Kernel::System::Type; use Kernel::System::Ticket; use Kernel::System::Queue; =head1 NAME StaticStatsTemplate.pm - the module that creates the stats about tickets in a queue =head1 SYNOPSIS All functions =head1 PUBLIC INTERFACE =over 4 =cut =item new() create an object use Kernel::Config; use Kernel::System::Encode; use Kernel::System::Log; use Kernel::System::Main; use Kernel::System::Time; use Kernel::System::DB; use Kernel::System::Stats::Static::StaticStatsTemplate; my $ConfigObject = Kernel::Config->new(); my $EncodeObject = Kernel::System::Encode->new( ConfigObject => $ConfigObject, ); my $LogObject = Kernel::System::Log->new( ConfigObject => $ConfigObject, ); my $MainObject = Kernel::System::Main->new( ConfigObject => $ConfigObject, LogObject => $LogObject, ); my $TimeObject = Kernel::System::Time->new( ConfigObject => $ConfigObject, LogObject => $LogObject, ); my $DBObject = Kernel::System::DB->new( ConfigObject => $ConfigObject, LogObject => $LogObject, MainObject => $MainObject, ); my $StatsObject = Kernel::System::Stats::Static::StaticStatsTemplate->new( ConfigObject => $ConfigObject, LogObject => $LogObject, MainObject => $MainObject, TimeObject => $TimeObject, DBObject => $DBObject, EncodeObject => $EncodeObject, ); =cut sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {%Param}; bless( $Self, $Type ); # check all needed objects for my $Needed ( qw(DBObject ConfigObject LogObject TimeObject MainObject EncodeObject) ) { $Self->{$Needed} = $Param{$Needed} || die "Got no $Needed"; } # create needed objects $Self->{TypeObject} = Kernel::System::Type->new( %{$Self} ); $Self->{TicketObject} = Kernel::System::Ticket->new( %{$Self} ); $Self->{QueueObject} = Kernel::System::Queue->new( %{$Self} ); return $Self; } =item Param() Get all parameters a user can specify. my @Params = $StatsObject->Param(); =cut sub Param { my $Self = shift; my %Queues = $Self->{QueueObject}->GetAllQueues(); my %Types = $Self->{TypeObject}->TypeList( Valid => 1, ); my @Params = ( { Frontend => 'Type', Name => 'TypeIDs', Multiple => 1, Size => 3, Data => \%Types, }, { Frontend => 'Queue', Name => 'QueueIDs', Multiple => 1, Size => 3, Data => \%Queues, }, ); return @Params; } =item Run() generate the statistic. my $StatsInfo = $StatsObject->Run( TypeIDs => [ 1, 2, 4 ], QueueIDs => [ 3, 4, 6 ], ); =cut sub Run { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(TypeIDs QueueIDs)) { if ( !$Param{$Needed} ) { $Self->{LogObject}->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # set report title my $Title = 'Tickets per Queue'; # table headlines my @HeadData = ( 'Ticket Number', 'Queue', 'Type', ); my @Data; my @TicketIDs = $Self->{TicketObject}->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', %Param, ); for my $TicketID ( @TicketIDs ) { my %Ticket = $Self->{TicketObject}->TicketGet( UserID => 1, TicketID => $TicketID, ); push @Data, [ $Ticket{TicketNumber}, $Ticket{Queue}, $Ticket{Type} ]; } return ( [$Title], [@HeadData], @Data ); } 1;
There is no configuration needed. Right after installation, the module is available to create a statistic for this module.
Standard OTRS versions 1.3 and 2.0 already facilitated the generation of stats. Various stats for OTRS versions 1.3 and 2.0 which have been specially developed to meet customers' requirements can be used in more recent versions too.
The files must merely be moved from the
Kernel/System/Stats/
path to
Kernel/System/Stats/Static/
. Additionally
the package name of the respective script must be amended by
"::Static".
The following example shows how the first path is amended.
package Kernel::System::Stats::AccountedTime;
package Kernel::System::Stats::Static::AccountedTime;
Ticket number generators are used to create distinct identifiers aka TicketNumber for new tickets. Any method of creating a string of numbers is possible, you should use common sense about the length of the resulting string (guideline: 5-10). When creating a ticket number, make sure the result is prefixed by the SysConfig-Variable SystemID in order to enable the detection of ticket numbers on inbound email responses. A ticket number generator module needs the two functions TicketCreateNumber() and GetTNByString(). The method TicketCreateNumber() is called without parameters and returns the new ticket number. The method GetTNByString() is called with the param String which contains the string to be parsed for a ticket number and returns the ticket number if found.
See Kernel/Config/Files/TicketNumberGenerator.xml in the package TemplateModule.
You should stick to the code of GetTNByString() as used in existing ticket number generators to prevent problems with ticket number parsing. Also the routine to detect a loop in TicketCreateNumber() should be kept intact to prevent duplicate ticket numbers.
Ticket event modules are running right after a ticket action takes place. Per convention these modules are located in the directory "Kernel/System/Ticket/Event". An ticket event module needs only the two functions new() and Run(). The method Run() receives at least the parameters Event, UserID, and Data. Data is a hash ref containing data of the ticket, and in case of Article-related events also containing Article data.
See Kernel/System/Ticket/Event/EventModulePostTemplate.pm in the package TemplateModule.
See Kernel/Config/Files/EventModulePostTemplate.xml in the package TemplateModule.
This standard feature has been implemented with the ticket event module Kernel::System::Ticket::Event::ForceUnlock. When this feature is not wanted, then it can be turned off by unsetting the SysConfig entry Ticket::EventModulePost###910-ForceUnlockOnMove.
A customized OTRS might hold non-standard data in additional database tables. When a ticket is deleted then this additional data needs to be deleted. This functionality can be achieved with a ticket event module listening to 'TicketDelete' events.
Ticket events have been available in OTRS since OTRS 2.0.
Ticket events available in OTRS 4.0:
TicketCreate
TicketDelete
TicketTitleUpdate
TicketUnlockTimeoutUpdate
TicketQueueUpdate
TicketTypeUpdate
TicketServiceUpdate
TicketSLAUpdate
TicketCustomerUpdate
TicketPendingTimeUpdate
TicketLockUpdate
TicketArchiveFlagUpdate
TicketStateUpdate
TicketOwnerUpdate
TicketResponsibleUpdate
TicketPriorityUpdate
HistoryAdd
HistoryDelete
TicketAccountTime
TicketMerge
TicketSubscribe
TicketUnsubscribe
TicketFlagSet
TicketFlagDelete
TicketSlaveLinkAdd
TicketSlaveLinkDelete
TicketMasterLinkDelete
EscalationResponseTimeNotifyBefore
EscalationUpdateTimeNotifyBefore
EscalationSolutionTimeNotifyBefore
EscalationResponseTimeStart
EscalationUpdateTimeStart
EscalationSolutionTimeStart
EscalationResponseTimeStop
EscalationUpdateTimeStop
EscalationSolutionTimeStop
Article events available in OTRS 4.0:
ArticleCreate
ArticleUpdate
ArticleSend
ArticleBounce
ArticleAgentNotification
ArticleCustomerNotification
ArticleAutoResponse
ArticleFlagSet
ArticleFlagDelete
ArticleAgentNotification
ArticleCustomerNotification
Dashboard module to display statistics in the form of a line graph.
# -- # Kernel/Output/HTML/DashboardTicketStatsGeneric.pm - message of the day # Copyright (C) 2001-2018 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::DashboardTicketStatsGeneric; use strict; use warnings; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {%Param}; bless( $Self, $Type ); # get needed objects for ( qw(Config Name ConfigObject LogObject DBObject LayoutObject ParamObject TicketObject UserID) ) { die "Got no $_!" if !$Self->{$_}; } return $Self; } sub Preferences { my ( $Self, %Param ) = @_; return; } sub Config { my ( $Self, %Param ) = @_; my $Key = $Self->{LayoutObject}->{UserLanguage} . '-' . $Self->{Name}; return ( %{ $Self->{Config} }, CacheKey => 'TicketStats' . '-' . $Self->{UserID} . '-' . $Key, ); } sub Run { my ( $Self, %Param ) = @_; my %Axis = ( '7Day' => { 0 => { Day => 'Sun', Created => 0, Closed => 0, }, 1 => { Day => 'Mon', Created => 0, Closed => 0, }, 2 => { Day => 'Tue', Created => 0, Closed => 0, }, 3 => { Day => 'Wed', Created => 0, Closed => 0, }, 4 => { Day => 'Thu', Created => 0, Closed => 0, }, 5 => { Day => 'Fri', Created => 0, Closed => 0, }, 6 => { Day => 'Sat', Created => 0, Closed => 0, }, }, ); my @Data; my $Max = 1; for my $Key ( 0 .. 6 ) { my $TimeNow = $Self->{TimeObject}->SystemTime(); if ($Key) { $TimeNow = $TimeNow - ( 60 * 60 * 24 * $Key ); } my ( $Sec, $Min, $Hour, $Day, $Month, $Year, $WeekDay ) = $Self->{TimeObject}->SystemTime2Date( SystemTime => $TimeNow, ); $Data[$Key]->{Day} = $Self->{LayoutObject}->{LanguageObject}->Get( $Axis{'7Day'}->{$WeekDay}->{Day} ); my $CountCreated = $Self->{TicketObject}->TicketSearch( # cache search result 20 min CacheTTL => 60 * 20, # tickets with create time after ... (ticket newer than this date) (optional) TicketCreateTimeNewerDate => "$Year-$Month-$Day 00:00:00", # tickets with created time before ... (ticket older than this date) (optional) TicketCreateTimeOlderDate => "$Year-$Month-$Day 23:59:59", CustomerID => $Param{Data}->{UserCustomerID}, Result => 'COUNT', # search with user permissions Permission => $Self->{Config}->{Permission} || 'ro', UserID => $Self->{UserID}, ); $Data[$Key]->{Created} = $CountCreated; if ( $CountCreated > $Max ) { $Max = $CountCreated; } my $CountClosed = $Self->{TicketObject}->TicketSearch( # cache search result 20 min CacheTTL => 60 * 20, # tickets with create time after ... (ticket newer than this date) (optional) TicketCloseTimeNewerDate => "$Year-$Month-$Day 00:00:00", # tickets with created time before ... (ticket older than this date) (optional) TicketCloseTimeOlderDate => "$Year-$Month-$Day 23:59:59", CustomerID => $Param{Data}->{UserCustomerID}, Result => 'COUNT', # search with user permissions Permission => $Self->{Config}->{Permission} || 'ro', UserID => $Self->{UserID}, ); $Data[$Key]->{Closed} = $CountClosed; if ( $CountClosed > $Max ) { $Max = $CountClosed; } } @Data = reverse @Data; my $Source = $Self->{LayoutObject}->JSONEncode( Data => \@Data, ); my $Content = $Self->{LayoutObject}->Output( TemplateFile => 'AgentDashboardTicketStats', Data => { %{ $Self->{Config} }, Key => int rand 99999, Max => $Max, Source => $Source, }, ); return $Content; } 1;
To use this module add the following to the Kernel/Config.pm
and restart your webserver (if you use mod_perl).
<ConfigItem Name="DashboardBackend###0250-TicketStats" Required="0" Valid="1"> <Description Lang="en">Parameters for the dashboard backend. "Group" are used to restricted access to the plugin (e. g. Group: admin;group1;group2;). "Default" means if the plugin is enabled per default or if the user needs to enable it manually. "CacheTTL" means the cache time in minutes for the plugin.</Description> <Description Lang="de">Parameter für das Dashboard Backend. "Group" ist verwendet um den Zugriff auf das Plugin einzuschränken (z. B. Group: admin;group1;group2;). ""Default" bedeutet ob das Plugin per default aktiviert ist oder ob dies der Anwender manuell machen muss. "CacheTTL" ist die Cache-Zeit in Minuten nach der das Plugin erneut aufgerufen wird.</Description> <Group>Ticket</Group> <SubGroup>Frontend::Agent::Dashboard</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::Output::HTML::DashboardTicketStatsGeneric</Item> <Item Key="Title">7 Day Stats</Item> <Item Key="Created">1</Item> <Item Key="Closed">1</Item> <Item Key="Permission">rw</Item> <Item Key="Block">ContentSmall</Item> <Item Key="Group"></Item> <Item Key="Default">1</Item> <Item Key="CacheTTL">45</Item> </Hash> </Setting> </ConfigItem>
An excessive number of days or individual lines may lead to performance degradation.
Notification modules are used to display a notification below the main navigation. You can write and register your own notification module. There are currently 5 ticket menues in the OTRS framework.
AgentOnline
AgentTicketEscalation
CharsetCheck
CustomerOnline
UIDCheck
The notification modules are located under Kernel/Output/HTML/TicketNotification*.pm. There is an example of a notify module below. Save it under Kernel/Output/HTML/TicketNotificationCustom.pm. You just need 2 functions: new() and Run().
# -- # Kernel/Output/HTML/NotificationCustom.pm # Copyright (C) 2001-2018 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::NotificationCustom; use strict; use warnings; use Kernel::System::Custom; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # get needed objects for my $Object (qw(ConfigObject LogObject DBObject LayoutObject TimeObject UserID)) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } $Self->{CustomObject} = Kernel::System::Custom->new(%Param); return $Self; } sub Run { my ( $Self, %Param ) = @_; # get session info my %CustomParam = (); my @Customs = $Self->{CustomObject}->GetAllCustomIDs(); my $IdleMinutes = $Param{Config}->{IdleMinutes} || 60 * 2; for (@Customs) { my %Data = $Self->{CustomObject}->GetCustomIDData( CustomID => $_, ); if ( $Self->{UserID} ne $Data{UserID} && $Data{UserType} eq 'User' && $Data{UserLastRequest} && $Data{UserLastRequest} + ( $IdleMinutes * 60 ) > $Self->{TimeObject}->SystemTime() && $Data{UserFirstname} && $Data{UserLastname} ) { $CustomParam{ $Data{UserID} } = "$Data{UserFirstname} $Data{UserLastname}"; if ( $Param{Config}->{ShowEmail} ) { $CustomParam{ $Data{UserID} } .= " ($Data{UserEmail})"; } } } for ( sort { $CustomParam{$a} cmp $CustomParam{$b} } keys %CustomParam ) { if ( $Param{Message} ) { $Param{Message} .= ', '; } $Param{Message} .= "$CustomParam{$_}"; } if ( $Param{Message} ) { return $Self->{LayoutObject}->Notify( Info => 'Custom Message: %s", "' . $Param{Message} ); } else { return ''; } } 1;
There is the need to activate your custom notification module. This can be done using the xml configuration below. There may be additional parameters in the config hash for your notification module.
<ConfigItem Name="Frontend::NotifyModule###3-Custom" Required="0" Valid="0"> <Description Lang="en">Module to show custom message in the agent interface.</Description> <Description Lang="de">Mit diesem Modul können eigene Meldungenen innerhalb des Agent-Interfaces angezeigt werden.</Description> <Group>Framework</Group> <SubGroup>Frontend::Agent::ModuleNotify</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::Output::HTML::NotificationCustom</Item> <Item Key="Key1">1</Item> <Item Key="Key2">2</Item> </Hash> </Setting> </ConfigItem>
Useful ticket menu implementation could be a link to a external tool if parameters (e.g. FreeTextField) have been set.
Ticket menu modules are used to display an addtional link in the menu above a ticket. You can write and register your own ticket menu module. There are 4 ticket menues (Generic, Lock, Responsible and TicketWatcher) which come with the OTRS framework. For more information please have a look at the OTRS admin manual.
The ticket menu modules are located under Kernel/Output/HTML/TicketMenu*.pm. There is an example of a ticket-menu module below. Save it under Kernel/Output/HTML/TicketMenuCustom.pm. You just need 2 functions: new() and Run().
# -- # Kernel/Output/HTML/TicketMenuCustom.pm # Copyright (C) 2001-2018 OTRS AG, https://otrs.com/ # -- # Id: TicketMenuCustom.pm,v 1.17 2010/04/12 21:34:06 martin Exp $ # -- # 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::TicketMenuCustom; use strict; use warnings; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # get needed objects for my $Object (qw(ConfigObject LogObject DBObject LayoutObject UserID TicketObject)) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } return $Self; } sub Run { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{Ticket} ) { $Self->{LogObject}->Log( Priority => 'error', Message => 'Need Ticket!' ); return; } # check if frontend module registered, if not, do not show action if ( $Param{Config}->{Action} ) { my $Module = $Self->{ConfigObject}->Get('Frontend::Module')->{ $Param{Config}->{Action} }; return if !$Module; } # check permission my $AccessOk = $Self->{TicketObject}->Permission( Type => 'rw', TicketID => $Param{Ticket}->{TicketID}, UserID => $Self->{UserID}, LogNo => 1, ); return if !$AccessOk; # check permission if ( $Self->{TicketObject}->CustomIsTicketCustom( TicketID => $Param{Ticket}->{TicketID} ) ) { my $AccessOk = $Self->{TicketObject}->OwnerCheck( TicketID => $Param{Ticket}->{TicketID}, OwnerID => $Self->{UserID}, ); return if !$AccessOk; } # check acl return if defined $Param{ACL}->{ $Param{Config}->{Action} } && !$Param{ACL}->{ $Param{Config}->{Action} }; # if ticket is customized if ( $Param{Ticket}->{Custom} eq 'lock' ) { # if it is locked for somebody else return if $Param{Ticket}->{OwnerID} ne $Self->{UserID}; # show custom action return { %{ $Param{Config} }, %{ $Param{Ticket} }, %Param, Name => 'Custom', Description => 'Custom to give it back to the queue!', Link => 'Action=AgentTicketCustom;Subaction=Custom;TicketID=$QData{"TicketID"}', }; } # if ticket is customized return { %{ $Param{Config} }, %{ $Param{Ticket} }, %Param, Name => 'Custom', Description => 'Custom it to work on it!', Link => 'Action=AgentTicketCustom;Subaction=Custom;TicketID=$QData{"TicketID"}', }; } 1;
There is the need to activate your custom ticket menu module. This can be done using the xml configuration below. There may be additional parameters in the config hash for your ticket menu module.
<ConfigItem Name="Ticket::Frontend::MenuModule###110-Custom" Required="0" Valid="1"> <Description Lang="en">Module to show custom link in menu.</Description> <Description Lang="de">Mit diesem Modul wird der Custom-Link in der Linkleiste der Ticketansicht angezeigt.</Description> <Group>Ticket</Group> <SubGroup>Frontend::Agent::Ticket::MenuModule</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::Output::HTML::TicketMenuCustom</Item> <Item Key="Name">Custom</Item> <Item Key="Action">AgentTicketCustom</Item> </Hash> </Setting> </ConfigItem>
Useful ticket menu implementation could be a link to a external tool if parameters (e.g. FreeTextField) have been set.
The ticket menu directs to an URL that can be handled. If you want to handle that request via the OTRS framework, you have to write your own frontend module.
The network transport is used as method to send and receive information between OTRS and a Remote System. The Generic Interface configuration allows a web service to use different network transport modules for provider and requester, but the most common scenario is that the same transport module is used for both.
OTRS as provider:
OTRS uses the network transport modules to get the data from the Remote System and the operation to be executed. After the operation is performed OTRS uses them again to send the response back to the Remote System.
OTRS as requester:
OTRS uses the network transport modules to send petitions to the Remote System to perform a remote action along with the required data, OTRS waits for the Remote System response and send it back to the Requester module.
In both ways network transport modules deals with the data in the Remote System format. it is not recommended to do any data transformation in this modules, the Mapping layer is the responsible to perform any data transformation needed during the communication. An exception of this is the data conversion that is required specifically by for the transport e.g. XML or JSON from / to Perl conversions.
Next will show how to develop a new transport backend, each transport backend has to implement these subroutines:
new
ProviderProcessRequest
ProviderGenerateResponse
RequesterPerformRequest
We should implement each one of this methods in order to be able to communicate correctly with a Remote System in both ways. All network transport backends are handled by the transport module (Kernel/GenericInterface/Transport.pm).
Currently Generic Interface implements the HTTP SOAP and HTTP REST transports, if the planned web service can use HTTP SOAP or HTTP SOAP there is no need to create a new network transport module, instead we recommend to take a look into HTTP SOAP or HTTP REST configurations to check their settings and how it can be tuned according to the remote system.
In case that the provided network transports does not match the web service needs, then in this section a sample network transport module is shown and each subroutine is explained. Normally transport modules uses CPAN modules as backends for example the HTTP SOAP transport modules uses SOAP::Lite module as backend.
For this example a custom package is used to return the data without doing a real network request to a Remote System, instead this custom module acts as a loop-back interface.
# -- # Kernel/GenericInterface/Transport/HTTP/Test.pm - GenericInterface network transport interface for testing # Copyright (C) 2001-2018 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::GenericInterface::Transport::HTTP::Test; use strict; use warnings; use HTTP::Request::Common; use LWP::UserAgent; use LWP::Protocol; # prevent 'Used once' warning for Kernel::OM use Kernel::System::ObjectManager; our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
transports can not be instantiated by the Object Manager.
sub new { my ( $Type, %Param ) = @_; my $Self = {}; bless( $Self, $Type ); for my $Needed (qw( DebuggerObject TransportConfig)) { $Self->{$Needed} = $Param{$Needed} || return { Success => 0, ErrorMessage => "Got no $Needed!" }; } return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines only objects of other classes not handled by the object manager that are needed in this module have to be created in "new".
sub ProviderProcessRequest { my ( $Self, %Param ) = @_; if ( $Self->{TransportConfig}->{Config}->{Fail} ) { return { Success => 0, ErrorMessage => "HTTP status code: 500", Data => {}, }; } my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request'); my %Result; for my $ParamName ( $ParamObject->GetParamNames() ) { $Result{$ParamName} = $ParamObject->GetParam( Param => $ParamName ); } # special handling for empty post request if ( scalar keys %Result == 1 && exists $Result{POSTDATA} && !$Result{POSTDATA} ) { %Result = (); } if ( !%Result ) { return $Self->{DebuggerObject}->Error( Summary => 'No request data found.', ); } return { Success => 1, Data => \%Result, Operation => 'test_operation', }; }
The 'ProviderProcessRequest' function gets the request form the Remote System (in this case the same OTRS) and extracts the data and the operation to perform from the request. For this example the operation is always 'test_operation'.
The way this function parse the request to get the data and the operation name, depends completely on the protocol to be implemented and the external modules that are used for.
sub ProviderGenerateResponse { my ( $Self, %Param ) = @_; if ( $Self->{TransportConfig}->{Config}->{Fail} ) { return { Success => 0, ErrorMessage => 'Test response generation failed', }; } my $Response; if ( !$Param{Success} ) { $Response = HTTP::Response->new( 500 => ( $Param{ErrorMessage} || 'Internal Server Error' ) ); $Response->protocol('HTTP/1.0'); $Response->content_type("text/plain; charset=UTF-8"); $Response->date(time); } else { # generate a request string from the data my $Request = HTTP::Request::Common::POST( 'http://testhost.local/', Content => $Param{Data} ); $Response = HTTP::Response->new( 200 => "OK" ); $Response->protocol('HTTP/1.0'); $Response->content_type("text/plain; charset=UTF-8"); $Response->add_content_utf8( $Request->content() ); $Response->date(time); } $Self->{DebuggerObject}->Debug( Summary => 'Sending HTTP response', Data => $Response->as_string(), ); # now send response to client print STDOUT $Response->as_string(); return { Success => 1, }; }
This function send the response back to the Remote System for the requested operation.
For this particular example we return an standard HTTP response success (200) or not (500), along with the required data on each case.
sub RequesterPerformRequest { my ( $Self, %Param ) = @_; if ( $Self->{TransportConfig}->{Config}->{Fail} ) { return { Success => 0, ErrorMessage => "HTTP status code: 500", Data => {}, }; } # use custom protocol handler to avoid sending out real network requests LWP::Protocol::implementor( testhttp => 'Kernel::GenericInterface::Transport::HTTP::Test::CustomHTTPProtocol' ); my $UserAgent = LWP::UserAgent->new(); my $Response = $UserAgent->post( 'testhttp://localhost.local/', Content => $Param{Data} ); return { Success => 1, Data => { ResponseContent => $Response->content(), }, }; }
This is the only function that is used by OTRS as requester. It sends the request to the Remote System and waits for it response.
For this example we use a custom protocol handler to avoid send the request to the real network. this custom protocol is specified below.
package Kernel::GenericInterface::Transport::HTTP::Test::CustomHTTPProtocol; use base qw(LWP::Protocol); sub new { my $Class = shift; return $Class->SUPER::new(@_); } sub request { ## no critic my $Self = shift; my ( $Request, $Proxy, $Arg, $Size, $Timeout ) = @_; my $Response = HTTP::Response->new( 200 => "OK" ); $Response->protocol('HTTP/1.0'); $Response->content_type("text/plain; charset=UTF-8"); $Response->add_content_utf8( $Request->content() ); $Response->date(time); #print $Request->as_string(); #print $Response->as_string(); return $Response; }
This is the code for the custom protocol that we use. This approach is only useful for training or for testing environments where the Remote Systems are not available.
For a new module development we do not recommend to use this approach, a real protocol needs to be implemented.
There is the need to register this network transport module to be accessible in the OTRS GUI. This can be done using the XML configuration below.
<ConfigItem Name="GenericInterface::Transport::Module###HTTP::Test" Required="0" Valid="1"> <Description Translatable="1">GenericInterface module registration for the transport layer.</Description> <Group>GenericInterface</Group> <SubGroup>GenericInterface::Transport::ModuleRegistration</SubGroup> <Setting> <Hash> <Item Key="Name">Test</Item> <Item Key="Protocol">HTTP</Item> <Item Key="ConfigDialog">AdminGenericInterfaceTransportHTTPTest</Item> </Hash> </Setting> </ConfigItem>
The mapping is used to convert data from / to OTRS, to / from the external system. This data can be represented as key => value pairs, mapping modules can be developed to transform not just values but also the keys.
For example:
From | To |
---|---|
Prio => Warning | PriorityID => 3 |
The mapping layer is not absolutely necessary, a web service can skip it completely depending on the web service configuration and how invokers and operation are implemented. But if some data transformations are needed, is highly recommended to use an existing mapping module or create a new one.
Mapping modules can be called more than one time during a normal communication, take a look to the following examples.
OTRS as provider example:
The remote system sends the request with the data in the remote system format
The data is mapped from the remote system format to the OTRS format
OTRS performs the operation and return the response in OTRS format
The data is mapped form the OTRS format to the remote system format
The response with the data in the remote system format is sent to the remote system
OTRS as requester example:
OTRS prepare the request to the remote system using the data in the OTRS format
The data is mapped from the OTRS format to the remote system format
The request is sent to the remote system which perform the action and send the response back to OTRS with the response data in remote system format
The data is mapped form remote system format (again) to the OTRS format
OTRS process the response
Generic Interface provides a mapping module called Simple. With this module most of the data transformations including key and value mapping can be done, and also it defines rules for to handling the default mappings for both keys and values.
So it is highly provable that you don't need to develop a custom mapping module. Please check Mapping Simple module (Kernel/GenericInterface/Mapping/Simple.pm) and its on-line documentation before continue.
If Mapping Simple module does not match your needs then we will show how to develop a new Mapping backend, each mapping backend has to implement these subroutines:
new
Map
We should implement each one of this methods in order to be able to map the data in the communication, handled either by the requester or provider. All mapping backends are handled by the mapping module (Kernel/GenericInterface/Mapping.pm).
In this section a sample mapping module is shown and each subroutine is explained.
# -- # Kernel/GenericInterface/Mapping/Test.pm - GenericInterface test data mapping backend # Copyright (C) 2001-2018 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::GenericInterface::Mapping::Test; use strict; use warnings; use Kernel::System::VariableCheck qw(IsHashRefWithData IsStringWithData); our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
We also include VariableCheck module to perform certain validation over some variables. Mappings can not be instantiated by the Object Manager.
sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed params for my $Needed (qw(DebuggerObject MappingConfig)) { if ( !$Param{$Needed} ) { return { Success => 0, ErrorMessage => "Got no $Needed!" }; } $Self->{$Needed} = $Param{$Needed}; } # check mapping config if ( !IsHashRefWithData( $Param{MappingConfig} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got no MappingConfig as hash ref with content!', ); } # check config - if we have a map config, it has to be a non-empty hash ref if ( defined $Param{MappingConfig}->{Config} && !IsHashRefWithData( $Param{MappingConfig}->{Config} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got MappingConfig with Data, but Data is no hash ref with content!', ); } return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines only objects of other classes not handled by the object manager that are needed in this module have to be created in "new".
sub Map { my ( $Self, %Param ) = @_; # check data - only accept undef or hash ref if ( defined $Param{Data} && ref $Param{Data} ne 'HASH' ) { return $Self->{DebuggerObject}->Error( Summary => 'Got Data but it is not a hash ref in Mapping Test backend!' ); } # return if data is empty if ( !defined $Param{Data} || !%{ $Param{Data} } ) { return { Success => 1, Data => {}, }; } # no config means that we just return input data if ( !defined $Self->{MappingConfig}->{Config} || !defined $Self->{MappingConfig}->{Config}->{TestOption} ) { return { Success => 1, Data => $Param{Data}, }; } # check TestOption format if ( !IsStringWithData( $Self->{MappingConfig}->{Config}->{TestOption} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got no TestOption as string with value!', ); } # parse data according to configuration my $ReturnData = {}; if ( $Self->{MappingConfig}->{Config}->{TestOption} eq 'ToUpper' ) { $ReturnData = $Self->_ToUpper( Data => $Param{Data} ); } elsif ( $Self->{MappingConfig}->{Config}->{TestOption} eq 'ToLower' ) { $ReturnData = $Self->_ToLower( Data => $Param{Data} ); } elsif ( $Self->{MappingConfig}->{Config}->{TestOption} eq 'Empty' ) { $ReturnData = $Self->_Empty( Data => $Param{Data} ); } else { $ReturnData = $Param{Data}; } # return result return { Success => 1, Data => $ReturnData, }; }
The 'map' function is the main part of each mapping module, it receives the mapping configuration (rules) and the data in the original format (either OTRS or remote system format) and converts it to a new format, even the structure of the data can be changed during the mapping process.
In this particular example there are three rules to map the values this rules are set in the mapping configuration key "TestOption" and they are ToUpper, ToLower and Empty.
ToUpper: converts each data value to upper case.
ToLower: converts each data value to lower case.
Empty: converts each data value into an empty string.
In this example no data key transformations were implemented.
sub _ToUpper { my ( $Self, %Param ) = @_; my $ReturnData = {}; for my $Key ( sort keys %{ $Param{Data} } ) { $ReturnData->{$Key} = uc $Param{Data}->{$Key}; } return $ReturnData; } sub _ToLower { my ( $Self, %Param ) = @_; my $ReturnData = {}; for my $Key ( sort keys %{ $Param{Data} } ) { $ReturnData->{$Key} = lc $Param{Data}->{$Key}; } return $ReturnData; } sub _Empty { my ( $Self, %Param ) = @_; my $ReturnData = {}; for my $Key ( sort keys %{ $Param{Data} } ) { $ReturnData->{$Key} = ''; } return $ReturnData; }
This are the helper functions that actually performs the string conversions
There is the need to register this mapping module to be accessible in the OTRS GUI. This can be done using the XML configuration below.
<ConfigItem Name="GenericInterface::Mapping::Module###Test" Required="0" Valid="1"> <Description Translatable="1">GenericInterface module registration for the mapping layer.</Description> <Group>GenericInterface</Group> <SubGroup>GenericInterface::Mapping::ModuleRegistration</SubGroup> <Setting> <Hash> <Item Key="ConfigDialog"></Item> </Hash> </Setting> </ConfigItem>
The invoker is used to create a request from OTRS to a Remote System, this part of the GI is in charge of perform necessary tasks in OTRS side, to gather the necessary data in order to construct the request.
Next we will show how to develop a new Invoker, each invoker has to implement these subroutines:
new
PrepareRequest
HandleResponse
We should implement each one of this methods in order to be able to execute a request using the request handler ('Kernel/GenericInterface/Requester.pm').
In this section a sample invoker module is shown and each subroutine is explained.
# -- # Kernel/GenericInterface/Invoker/Test.pm - GenericInterface test data Invoker backend # Copyright (C) 2001-2018 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::GenericInterface::Invoker::Test::Test; use strict; use warnings; use Kernel::System::VariableCheck qw(IsString IsStringWithData); # prevent 'Used once' warning for Kernel::OM use Kernel::System::ObjectManager; our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package name is declared via the package keyword. Invokers can not be instantiated by the Object Manager.
sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed params if ( !$Param{DebuggerObject} ) { return { Success => 0, ErrorMessage => "Got no DebuggerObject!" }; } $Self->{DebuggerObject} = $Param{DebuggerObject}; return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines only objects of other classes not handled by the object manager that are needed in this module have to be created in "new".
sub PrepareRequest { my ( $Self, %Param ) = @_; # we need a TicketNumber if ( !IsStringWithData( $Param{Data}->{TicketNumber} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got no TicketNumber' ); } my %ReturnData; $ReturnData{TicketNumber} = $Param{Data}->{TicketNumber}; # check Action if ( IsStringWithData( $Param{Data}->{Action} ) ) { $ReturnData{Action} = $Param{Data}->{Action} . 'Test'; } # check request for system time if ( IsStringWithData( $Param{Data}->{GetSystemTime} ) && $Param{Data}->{GetSystemTime} ) { $ReturnData{SystemTime} = $Kernel::OM->Get('Kernel::System::Time')->SystemTime(); } return { Success => 1, Data => \%ReturnData, }; }
The 'PrepareRequest' function is used to handle and collect all needed data to be sent into the request, here we can receive data from the request handler, use it, extend it, generate new data, and after that, we can transfer the results to the mapping layer.
For this example we are expecting to receive a ticket number, if there isn't then we use the debugger method "Error()" that creates an entry in the debug log and also returns a structure with the parameter Success as 0 and Error Message as the passed summary.
Also this example appends the word "Test" to the parameter "Action" and if GetSystemTime is requested, it will fill the SystemTime parameter with the current system time. This part of the code is to prepare the data to be sent, on a real invoker some calls to core modules (Kernel/System/*.pm) should be made here.
If during any part of the 'PrepareRequest' function the request need to be stop without generating and error an entry in the debug log the following code can be used:
# stop requester communication return { Success => 1, StopCommunication => 1, };
Using this, the Requester will understand that the request should not continue ( it will not be sent to Mapping layer and will also not be sent to the Network Transport) Requester will not send an error on the debug log. it will only silently stop.
sub HandleResponse { my ( $Self, %Param ) = @_; # if there was an error in the response, forward it if ( !$Param{ResponseSuccess} ) { if ( !IsStringWithData( $Param{ResponseErrorMessage} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got response error, but no response error message!', ); } return { Success => 0, ErrorMessage => $Param{ResponseErrorMessage}, }; } # we need a TicketNumber if ( !IsStringWithData( $Param{Data}->{TicketNumber} ) ) { return $Self->{DebuggerObject}->Error( Summary => 'Got no TicketNumber!' ); } # prepare TicketNumber my %ReturnData = ( TicketNumber => $Param{Data}->{TicketNumber}, ); # check Action if ( IsStringWithData( $Param{Data}->{Action} ) ) { if ( $Param{Data}->{Action} !~ m{ \A ( .*? ) Test \z }xms ) { return $Self->{DebuggerObject}->Error( Summary => 'Got Action but it is not in required format!', ); } $ReturnData{Action} = $1; } return { Success => 1, Data => \%ReturnData, }; }
The 'HandleResponse' function is used to receive and process the data from the previous request, that was made to the Remote System this data already passed by Mapping layer, to transform it from Remote System format to OTRS format (if needed).
For this particular example it checks the ticket number again and check if the action ends with the word 'Test' (as was done in the 'PrepareRequest' function).
Remember This invoker is only used for tests, a real invoker will check if the response was on the format described by the Remote System and can perform some actions like: call another invoker, perform an call to a Core Module, update the database, send an error, etc.
There is the need to register this invoker module to be accessible in the OTRS GUI. This can be done using the XML configuration below.
<ConfigItem Name="GenericInterface::Invoker::Module###Test::Test" Required="0" Valid="1"> <Description Translatable="1">GenericInterface module registration for the invoker layer.</Description> <Group>GenericInterface</Group> <SubGroup>GenericInterface::Invoker::ModuleRegistration</SubGroup> <Setting> <Hash> <Item Key="Name">Test</Item> <Item Key="Controller">Test</Item> <Item Key="ConfigDialog">AdminGenericInterfaceInvokerDefault</Item> </Hash> </Setting> </ConfigItem>
The operation is used to perform an action within OTRS, this action is requested by the external system and can include special parameters in order to correctly execute the action. After the action is performed, OTRS sends a defined confirmation to the external system.
Next we will show how to develop a new Operation, each operation has to implement these subroutines
new
Run
We should implement each one of this methods in order to be able to execute the action handled by the provider ('Kernel/GenericInterface/Provider.pm').
In this section a sample operation module is shown and each subroutine is explained.
# -- # Kernel/GenericInterface/Operation/Test/Test.pm - GenericInterface test operation backend # Copyright (C) 2001-2018 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::GenericInterface::Operation::Test::Test; use strict; use warnings; use Kernel::System::VariableCheck qw(IsHashRefWithData); our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
We also include VariableCheck module to perform certain validation over some variables. Operations can not be instantiated by the Object Manager.
sub new { my ( $Type, %Param ) = @_; my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Needed (qw(DebuggerObject)) { if ( !$Param{$Needed} ) { return { Success => 0, ErrorMessage => "Got no $Needed!" }; } $Self->{$Needed} = $Param{$Needed}; } return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines only objects of other classes not handled by the object manager that are needed in this module have to be created in "new".
sub Run { my ( $Self, %Param ) = @_; # check data - only accept undef or hash ref if ( defined $Param{Data} && ref $Param{Data} ne 'HASH' ) { return $Self->{DebuggerObject}->Error( Summary => 'Got Data but it is not a hash ref in Operation Test backend)!' ); } if ( defined $Param{Data} && $Param{Data}->{TestError} ) { return { Success => 0, ErrorMessage => "Error message for error code: $Param{Data}->{TestError}", Data => { ErrorData => $Param{Data}->{ErrorData}, }, }; } # copy data my $ReturnData; if ( ref $Param{Data} eq 'HASH' ) { $ReturnData = \%{ $Param{Data} }; } else { $ReturnData = undef; } # return result return { Success => 1, Data => $ReturnData, }; }
The 'Run' function is the main part of each operation, it receives all internal mapped data from remote system needed by the provider to execute the action, it performs the action and returns the result to the provider to be external mapped and deliver back to the remote system.
This particular example returns the same data as came from the remote system, unless "TestError" parameter is passed in this case it returns an error.
There is the need to register this operation module to be accessible in the OTRS GUI. This can be done using the XML configuration below.
<ConfigItem Name="GenericInterface::Operation::Module###Test::Test" Required="0" Valid="1"> <Description Translatable="1">GenericInterface module registration for the operation layer.</Description> <Group>GenericInterface</Group> <SubGroup>GenericInterface::Operation::ModuleRegistration</SubGroup> <Setting> <Hash> <Item Key="Name">Test</Item> <Item Key="Controller">Test</Item> <Item Key="ConfigDialog">AdminGenericInterfaceOperationDefault</Item> </Hash> </Setting> </ConfigItem>
The task handler modules are used to perform actions within OTRS asynchronously via OTRS scheduler (a separated "daemon like" process ) made specifically to execute this kind of jobs.
Next we will see how to develop a new scheduler task handler backend. Each task handler backend has to implement these subroutines:
new
Run
We should implement each one of this methods in order to be able to execute tasks. All task handler backends are used by main scheduler task handler module (Kernel/scheduler/TaskHandler.pm).
Currently the scheduler only has one working task handler backend named "GenericInterface". This task handler backend executes Generic Interface Invoker modules in the background.
For example:
By using the OTRS Scheduler combined with the Generic Interface a user does not need to wait in the New phone ticket screen after creating a ticket until a Remote System responds to a web service request triggered by an event like "TicketCreate", instead this task can be delegated to the OTRS scheduler to run it on the background.
There is no need to create a new task handler backend for the Generic Interface, unless a different behavior from the current one is needed. to delegate Generic Interface Invoker tasks to the OTRS Scheduler you need to configure the Invoker event trigger as asynchronous.
In this section a sample scheduler task handler module is shown and each subroutine is explained.
# -- # Kernel/Scheduler/TaskHandler/Test.pm - Scheduler task handler test backend # Copyright (C) 2001-2018 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::Scheduler::TaskHandler::Test; use strict; use warnings; use vars qw(@ISA);
This is common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
sub new { my ( $Type, %Param ) = @_; my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Needed (qw(MainObject ConfigObject LogObject DBObject TimeObject)) { $Self->{$Needed} = $Param{$Needed} || die "Got no $Needed!"; } return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines objects of other classes that are needed in this module have to be created in "new".
sub Run { my ( $Self, %Param ) = @_; # check data - we need a hash ref if ( $Param{Data} && ref $Param{Data} ne 'HASH' ) { $Self->{LogObject}->Log( Priority => 'error', Message => 'Got no valid Data!', ); return { Success => 0, }; } # create tmp file if ( $Param{Data}->{File} ) { my $Content = 123; return if !$Self->{MainObject}->FileWrite( Location => $Param{Data}->{File}, Content => \$Content, ); } # re schedule with new time return { Success => $Param{Data}->{Success}, ReSchedule => $Param{Data}->{ReSchedule}, DueTime => $Param{Data}->{ReScheduleDueTime}, Data => $Param{Data}->{ReScheduleData}, }; }
The 'Run' function is the main part of the module it checks the incoming data, make calls to other OTRS modules to execute the tasks, waits for the task result and based on the result information it determines if the task was successfully executed or not and if the task needs to be re-schedule itself, when and with which data.
For this testing example if the parameter "File" exists in the incoming data, a new file is to be created on the file system by the task handler, depending of the success of this, the task handler will continue to the rest part of the code where base it decisions for the returning structure on the previously provided data.
On real task handler backends the decisions for the returning structure could be based on the task handler backend logics, the task execution results, configuration settings, etc.
Dynamic Fields are custom fields that can be added to a screen to enhance and add information to an object (e.g. a ticket or an article).
The Dynamic Fields are the evolution of the ticket and article Free Fields ( "TikcetFreeText", "TicketFreeKey", "TicketFreeTime", "ArticleFreeText", "ArticleFreeKey" and "ArticleFreeTime" ) from older versions of OTRS.
From OTRS version 3.1 the old Free Fields has been replaced with the new Dynamic Fields, for a better backward compatibility and data preservation when updating from previous versions, a migration script has been developed to convert the existing Free Fields to Dynamic Fields and to move their values from the ticket and article tables in the database to new dynamic fields tables.
Any custom development that uses Free Fields needs to be ported to the new Dynamic Fields code structure, otherwise it will not work anymore. For this reason is very important to know that only updated installations of OTRS 3.0 has the old Free Fields converted to Dynamic Fields, new or clean installations of OTRS has no Dynamic Fields defined "out of the box" and any Dynamic Field needed by the custom development needs to be added.
The restriction on the number of the fields per ticket or article has been removed, this means that a ticket or article could have as many fields as needed. and now it is also possible to use the Dynamic Fields framework for other objects rather than just ticket or article.
The new Dynamic Fields can store the same data types as the Free Fields (Text and Date/Time), and they can be also defined as them (Single line input, drop-down and date-time). but Dynamic Fields goes beyond that, a new data type: Integer, has been added and also new options to define the fields like Multiple-line inputs, check-boxes, Multiple-select and date (without time) fields. Each field type defines its own data type.
Due to its modular design each Dynamic Field type can be seen as a plug-in to a framework, and this plug-in can be a OTRS standard package to extend the available types of the Dynamic Fields or even to extend current Dynamic Field with more functions.
Before creating new Dynamic Fields is necessary to understand its framework and how OTRS screens interact with them, as well as their underlying API.
The following picture shows the architecture of the Dynamic Fields framework.
Normally called as "BackendObject" in the frontend modules is the mediator between the frontend modules and each specific Dynamic Field implementation or Driver. It defines a Generic middle API for all Dynamic Field Drivers, and each Driver has the responsibility to implement the middle API for the specific needs for the field.
The Dynamic Field Backend is the master controller of all the Drivers, each function in this module is responsible to check the required parameters and call the same function in the specific Driver according to the Dynamic Field Configuration parameter received.
This module is also responsible to call specific functions on each Object Type Delegate (like Ticket or Article) e.g. to add a history entry or fire an event.
This module is located in $OTRS_HOME/Kernel/System/DynamicField/Backend.pm.
A Dynamic Field Driver is the implementation of the Dynamic Field. Each Driver must implement all the mandatory functions specified in the Backend. (there are some functions that depends on a behavior and it is not needed to implement those if the Dynamic Field does not have that particular behavior)
A Driver is responsible to know how to get its own value or values from a web request, or from a profile (like a search profile), it also needs to know the HTML code to render the field in edit or display screens, or how to interact with the stats module, among other functions.
This modules are located in $OTRS_HOME/Kernel/System/DynamicField/Driver/*.pm.
It exists some base drivers like Base.pm BaseText.pm, BaseSelect.pm and BaseDateTime.pm, that implements common functions for certain drivers (e.g Driver TextArea.pm uses BaseText.pm that also uses Base.pm then TextArea only needs to implement the functions that are missing in Base.pm and BateText.pm or the ones that are special cases)
The following is the Drivers inheritance tree:
Base.pm
BaseText.pm
Text.pm
TextArea.pm
BaseSelect.pm
Dropdown.pm
Multiselect.pm
BaseDateTime.pm
DateTime.pm
Date.pm
Checkbox.pm
To manage the Dynamic Fields (Add, Edit and List) a series of modules has been already developed. There is one specific master module (AdminDynamicField.pm) that shows the list of defined Dynamic Fields, and from within other modules are called to create new Dynamic Fields or modify an existing ones.
Normally a Dynamic Field Driver needs its own Admin Module (Admin Dialog) to define its properties, this dialog might differ from other Drivers. But this is not mandatory, Drivers can share Admin Dialogs, if they can provide add needed information for all the Drivers that are linked to them, no matter if they are from different type. What is mandatory is that each Driver must be linked to an Admin Dialog. (e.g. Text and TextArea Drivers share AdminDynamicFieldText.pm Admin Dialog, and Date and Date/Time Drivers share AdminDynamicFieldDateTime.pm Admin Dialog).
Admin Dialogs follows the normal OTRS Admin Module rules and architecture. But for standardization all configuration common parts to all Dynamic Fields should have the same look and feel among all Admin Dialogs.
This modules are located in $OTRS_HOME/Kernel/Modules/*.pm.
Each Admin Dialog needs its corresponding HTML template file (.tt).
This modules reads and writes the Dynamic Fields information from and to the database tables.
This module is responsible to manage the Dynamic Field definitions, it provides the basic API for add, change, delete, list and get Dynamic Fields and is located in $OTRS_HOME/Kernel/System/DynamicField.pm.
There are two tables in the database to store the dynamic field information:
dynamic_field: Used by the Core Module DynamicField.pm, it stores the Dynamic Field definitions.
dynamic_field_value: Used by the Core Module DynamicFieldValue.pm to save the Dynamic Field values for each Dynamic Field and each Object Type instance.
The Backend module needs a way to know which Drivers exists and since the amount of Drivers can be easily extended, the easiest way to manage them is to use the system configuration, where the information of Dynamic Field Drivers and ObjectType Drivers can be stored and extended.
The master Admin Module also needs to know this information about the available Dynamic Field Drivers to use the Admin Dialog linked with, to create or modify the Dynamic Fields.
Frontend modules needs to read the system configuration to know which Dynamic Fields are active for each screen and which ones are also mandatory. for example: Ticket::Frontend::AgentTicketPhone###DynamicField stores the active, mandatory and inactive Dynamic Fields for New Phone Ticket Screen.
Knowing about how Frontend modules interact with Dynamic fields is not strictly necessary to extend Dynamic Fields for the Ticket or Article objects, since all the screens that could use the Dynamic Fields are already prepared. But in case of custom developments or to extend the Dynamic Fields to other objects is very useful to know how to access Dynamic Fields framework from a Frontend Module.
The following picture shows a simple example of how the Dynamic Fields interact with other OTRS framework parts.
The first step is that the Frontend module reads the configured Dynamic Fields for example AgentTicketNote should read Ticket::Frontend::AgentTicketNote###DynamicField setting. This setting can be used as the filter parameter for DynamicField Core Module function DynamicFieldListGet(). The screen can store the results of this function to have the list of the Dynamic Fields activated for this particular screen.
Next, the screen should try to get the values from the web request, it can use the BackendObject function EditFieldValueGet() for this purpose. and can use this values to trigger ACLs. The BackendObject will use each Driver to perform the specific actions for all functions.
To continue, the screen should get the HTML for each field to display it, the BackendObject function EditFieldRender() can be used to perform this action and the ACLs restriction as well as the Values from the web request can be passed to this function in order to get better results. in case of a submit the screen could also use the BackendObject function EditFieldValueValidate() to check the mandatory fields.
Other screens could use DisplayFieldRender() instead of EditFieldRender() if the screen only shows the field value, and in such case no value validation is needed.
To store the value of the Dynamic Field is necessary to get the Object ID, for this example if the Dynamic Field is linked to a ticket object, the screen should already have the TicketID, otherwise if the field is linked to an article object in order to set the value of the field is necessary to create the article first. ValueSet() from the BackendObject can be used to set the Dynamic Field value.
In summary the Frontend modules does not need to know how each Dynamic Field works internally to get or set their values or to display them, It just needs to call the BackendObject module and use the fields in a generic way.
There are many ways to extend the Dynamic Fields, the following sections will try to cover the most common scenarios.
To create a new Dynamic Field Type is necessary to:
Create a Dynamic Field Driver:
This is the main module of the new field.
Create or use an existing Admin Dialog:
To have a management interface and set its configuration options.
Create a Configuration File:
To register the new field in the Backend (or new Admin Dialogs in the framework if needed) and be able to create instances or it.
To create a new Dynamic Field Type for other objects is necessary to:
Create a Dynamic Field Driver
This is the main module of the new Field.
Create an Object Type Delegate
This is necessary, even if the "other object" does not require any specific data handling in its functions (e.g. after a value is set). All Object Type Delegates must implement the functions that the Backend requires.
Take a look in the current Object Type Delegates to implement the same functions, even if they just return a successful value for the "other object".
Create or use an existing Admin Dialog
To have a management interface and set its configuration options.
Implement Dynamic Fields in the Frontend Modules
To be able to use the Dynamic Fields.
Create a Configuration File
To register the new field in the Backend (or new Admin Dialogs in the framework if needed) and be able to create instances or it. And make the needed settings to show, hide or show the Dynamic Fields as Mandatory in the new screens.
To create a package to use existing dynamic fields is necessary to:
Implement Dynamic Fields in the Frontend Modules
To be able to use the Dynamic Fields.
Create a Configuration File
To give the end user the possibility to show, hide or show the Dynamic Fields as Mandatory in the new screens.
It might be possible that the BackendObject does not have a needed function for custom developments, or could also be possible that it has the function needed, but the return format does not match the needs of the custom development, or that a new behavior is needed to execute the new or the old functions.
The easiest way to do this, is to extend the current field files, for this is necessary to create a new Backend extension file that defines the new functions and create also Drivers extensions that implement this new functions for each field. This new drivers will only need to implement the new functions since the original drivers takes care of the standard functions. All this new new files does not need a constructor as they will be loaded as a base for the BackendObject and the drivers.
The only restrictions are that the functions should be named different than the ones on the Backend and Drivers, otherwise they will be overwritten with current objects.
Put the new Backend extension into the DynamicField directory (e.g. /$OTRS_HOME/Kernel/System/DynamicField/NewPackageBackend.pm and its Drivers in /$OTRS_HOME/Kernel/System/DynamicField/Driver/NewPackage*.pm
New behaviors only need a small setting in the extensions configuration file.
To create new Backend functions is needed to:
Create a New Backend extension module.
To define only the new functions.
Create the Dynamic Fields Driver extensions.
To implement only the new functions.
Implement New Dynamic Fields functions in the Frontend Modules
To be able to use the new Dynamic Fields functions.
Create a Configuration File
To register the new backend and drivers extensions and behaviors.
To illustrate this process a new Dynamic Field "Password" will be created. this new Dynamic Field Type will show a New password field to Ticket or Article objects. since is very similar to a Text Dynamic Field we will use the Base an BaseText Drivers as a basis to build this new field.
This new password field implementation is just for educational purposes, it does not provide any level of security and is not recommended for production systems.
To create this new Dynamic Field we will create 4 files: a Configuration File (XML), to register the modules, an Admin Dialog Module (Perl), to setup the field options, a template module, for the Admin Dialog and a Dynamic Field Driver (Perl).
File Structure:
$HOME (e. g. /opt/otrs/) | ... |--/Kernel/ | |--/Config/ | | |--/Files/ | | | |DynamicFieldPassword.xml ... | |--/Modules/ | | |AdminDynamicFieldPassword.pm ... | |--/Output/ | | |--/HTML/ | | | |--/Standard/ | | | | |AdminDynamicFieldPassword.tt ... | |--/System/ | | |--/DynamicField/ | | | |--/Driver/ | | | | |Password.pm ...
The configuration files are used to register the Dynamic Field Types (Driver) and the Object Type Drivers for the BackendObject. They also store standard registrations for Admin Modules in the framework.
In this section a configuration file for password Dynamic Field is shown and explained.
<?xml version="1.0" encoding="utf-8"?> <otrs_config version="1.0" init="Application">
This is the normal header for a configuration file.
<ConfigItem Name="DynamicFields::Driver###Password" Required="0" Valid="1"> <Description Translatable="1">DynamicField backend registration.</Description> <Group>DynamicFieldPassword</Group> <SubGroup>DynamicFields::Backend::Registration</SubGroup> <Setting> <Hash> <Item Key="DisplayName" Translatable="1">Password</Item> <Item Key="Module">Kernel::System::DynamicField::Driver::Password</Item> <Item Key="ConfigDialog">AdminDynamicFieldPassword</Item> </Hash> </Setting> </ConfigItem>
This setting registers the Password Dynamic Field Driver for the Backend module so it can be included in the list of available Dynamic Fields Types, it also specify its own Admin Dialog in the key "ConfigDialog", this key is used by the Master Dynamic Field Admin Module to manage this new Dynamic Field Type.
<ConfigItem Name="Frontend::Module###AdminDynamicFieldPassword" Required="0" Valid="1"> <Description Translatable="1">Frontend module registration for the agent interface.</Description> <Group>DynamicFieldPassword</Group> <SubGroup>Frontend::Admin::ModuleRegistration</SubGroup> <Setting> <FrontendModuleReg> <Group>admin</Group> <Description>Admin</Description> <Title Translatable="1">Dynamic Fields Text Backend GUI</Title> <Loader> <JavaScript>Core.Agent.Admin.DynamicField.js</JavaScript> </Loader> </FrontendModuleReg> </Setting> </ConfigItem>
This is an standard module registration for the Password Admin Dialog in the Admin Interface.
</otrs_config>
Standard closure of a configuration file.
The Admin Dialogs are standard Admin modules to manage (add or edit) the Dynamic Fields.
In this section an Admin Dialog for password dynamic field is shown and explained.
# -- # Kernel/Modules/AdminDynamicFieldPassword.pm - provides a dynamic fields password config view for admins # Copyright (C) 2001-2018 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::Modules::AdminDynamicFieldPassword; use strict; use warnings; use Kernel::System::VariableCheck qw(:all); use Kernel::System::Valid; use Kernel::System::CheckItem; use Kernel::System::DynamicField;
This is the common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
sub new { my ( $Type, %Param ) = @_; my $Self = {%Param}; bless( $Self, $Type ); for (qw(ParamObject LayoutObject LogObject ConfigObject)) { if ( !$Self->{$_} ) { $Self->{LayoutObject}->FatalError( Message => "Got no $_!" ); } } # create additional objects $Self->{ValidObject} = Kernel::System::Valid->new( %{$Self} ); $Self->{DynamicFieldObject} = Kernel::System::DynamicField->new( %{$Self} ); # get configured object types $Self->{ObjectTypeConfig} = $Self->{ConfigObject}->Get('DynamicFields::ObjectType'); # get the fields config $Self->{FieldTypeConfig} = $Self->{ConfigObject}->Get('DynamicFields::Backend') || {}; $Self->{DefaultValueMask} = '****'; return $Self; }
The constructor 'new' creates a new instance of the class. According to the coding guidelines objects of other classes that are needed in this module are normally created here.
sub Run { my ( $Self, %Param ) = @_; if ( $Self->{Subaction} eq 'Add' ) { return $Self->_Add( %Param, ); } elsif ( $Self->{Subaction} eq 'AddAction' ) { # challenge token check for write action $Self->{LayoutObject}->ChallengeTokenCheck(); return $Self->_AddAction( %Param, ); } if ( $Self->{Subaction} eq 'Change' ) { return $Self->_Change( %Param, ); } elsif ( $Self->{Subaction} eq 'ChangeAction' ) { # challenge token check for write action $Self->{LayoutObject}->ChallengeTokenCheck(); return $Self->_ChangeAction( %Param, ); } return $Self->{LayoutObject}->ErrorScreen( Message => "Undefined subaction.", ); }
Run is the default function to be called by the web request, we try to make this function as simple as possible and let the helper functions to do the "hard" work.
sub _Add { my ( $Self, %Param ) = @_; my %GetParam; for my $Needed (qw(ObjectType FieldType FieldOrder)) { $GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed ); if ( !$Needed ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need $Needed", ); } } # get the object type and field type display name my $ObjectTypeName = $Self->{ObjectTypeConfig}->{ $GetParam{ObjectType} }->{DisplayName} || ''; my $FieldTypeName = $Self->{FieldTypeConfig}->{ $GetParam{FieldType} }->{DisplayName} || ''; return $Self->_ShowScreen( %Param, %GetParam, Mode => 'Add', ObjectTypeName => $ObjectTypeName, FieldTypeName => $FieldTypeName, ); }
_Add() function is also pretty simple, it just get some parameters from the web request and call the _ShowScreen() function, normally this function is not needed to be modified.
sub _AddAction { my ( $Self, %Param ) = @_; my %Errors; my %GetParam; for my $Needed (qw(Name Label FieldOrder)) { $GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed ); if ( !$GetParam{$Needed} ) { $Errors{ $Needed . 'ServerError' } = 'ServerError'; $Errors{ $Needed . 'ServerErrorMessage' } = 'This field is required.'; } } if ( $GetParam{Name} ) { # check if name is alphanumeric if ( $GetParam{Name} !~ m{\A ( ?: [a-zA-Z] | \d )+ \z}xms ) { # add server error error class $Errors{NameServerError} = 'ServerError'; $Errors{NameServerErrorMessage} = 'The field does not contain only ASCII letters and numbers.'; } # check if name is duplicated my %DynamicFieldsList = %{ $Self->{DynamicFieldObject}->DynamicFieldList( Valid => 0, ResultType => 'HASH', ) }; %DynamicFieldsList = reverse %DynamicFieldsList; if ( $DynamicFieldsList{ $GetParam{Name} } ) { # add server error error class $Errors{NameServerError} = 'ServerError'; $Errors{NameServerErrorMessage} = 'There is another field with the same name.'; } } if ( $GetParam{FieldOrder} ) { # check if field order is numeric and positive if ( $GetParam{FieldOrder} !~ m{\A ( ?: \d )+ \z}xms ) { # add server error error class $Errors{FieldOrderServerError} = 'ServerError'; $Errors{FieldOrderServerErrorMessage} = 'The field must be numeric.'; } } for my $ConfigParam ( qw( ObjectType ObjectTypeName FieldType FieldTypeName DefaultValue ValidID ShowValue ValueMask ) ) { $GetParam{$ConfigParam} = $Self->{ParamObject}->GetParam( Param => $ConfigParam ); } # uncorrectable errors if ( !$GetParam{ValidID} ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need ValidID", ); } # return to add screen if errors if (%Errors) { return $Self->_ShowScreen( %Param, %Errors, %GetParam, Mode => 'Add', ); } # set specific config my $FieldConfig = { DefaultValue => $GetParam{DefaultValue}, ShowValue => $GetParam{ShowValue}, ValueMask => $GetParam{ValueMask} || $Self->{DefaultValueMask}, }; # create a new field my $FieldID = $Self->{DynamicFieldObject}->DynamicFieldAdd( Name => $GetParam{Name}, Label => $GetParam{Label}, FieldOrder => $GetParam{FieldOrder}, FieldType => $GetParam{FieldType}, ObjectType => $GetParam{ObjectType}, Config => $FieldConfig, ValidID => $GetParam{ValidID}, UserID => $Self->{UserID}, ); if ( !$FieldID ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Could not create the new field", ); } return $Self->{LayoutObject}->Redirect( OP => "Action=AdminDynamicField", ); }
The _AddAction() function gets the configuration parameters from a new dynamic field, it validates the Dynamic Field name only contains letters and numbers. This function could validate any other parameter.
Name, Label, FieldOrder, Validity are common parameters for all Dynamic Fields and they are required. Each Dynamic Field has its specific configuration that must contain at least the DefaultValue parameter, in this case it also have ShowValue and ValueMask parameters for Password field.
If the field has the ability to store a fixed list of values they need should be stored in the PossibleValues parameter inside the specific configuration hash.
As in other Admin Modules, if a parameter is not valid this function returns to the Add screen highlighting the erroneous form fields.
If all the parameters are correct it creates a new Dynamic Field.
sub _Change { my ( $Self, %Param ) = @_; my %GetParam; for my $Needed (qw(ObjectType FieldType)) { $GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed ); if ( !$Needed ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need $Needed", ); } } # get the object type and field type display name my $ObjectTypeName = $Self->{ObjectTypeConfig}->{ $GetParam{ObjectType} }->{DisplayName} || ''; my $FieldTypeName = $Self->{FieldTypeConfig}->{ $GetParam{FieldType} }->{DisplayName} || ''; my $FieldID = $Self->{ParamObject}->GetParam( Param => 'ID' ); if ( !$FieldID ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need ID", ); } # get dynamic field data my $DynamicFieldData = $Self->{DynamicFieldObject}->DynamicFieldGet( ID => $FieldID, ); # check for valid dynamic field configuration if ( !IsHashRefWithData($DynamicFieldData) ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Could not get data for dynamic field $FieldID", ); } my %Config = (); # extract configuration if ( IsHashRefWithData( $DynamicFieldData->{Config} ) ) { %Config = %{ $DynamicFieldData->{Config} }; } return $Self->_ShowScreen( %Param, %GetParam, %${DynamicFieldData}, %Config, ID => $FieldID, Mode => 'Change', ObjectTypeName => $ObjectTypeName, FieldTypeName => $FieldTypeName, ); }
The "_Change" function is very similar to the "_Add" function but since this function is used to edit an existing field it needs to validated the FieldID parameter and gather the current Dynamic Field data.
sub _ChangeAction { my ( $Self, %Param ) = @_; my %Errors; my %GetParam; for my $Needed (qw(Name Label FieldOrder)) { $GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed ); if ( !$GetParam{$Needed} ) { $Errors{ $Needed . 'ServerError' } = 'ServerError'; $Errors{ $Needed . 'ServerErrorMessage' } = 'This field is required.'; } } my $FieldID = $Self->{ParamObject}->GetParam( Param => 'ID' ); if ( !$FieldID ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need ID", ); } if ( $GetParam{Name} ) { # check if name is lowercase if ( $GetParam{Name} !~ m{\A ( ?: [a-zA-Z] | \d )+ \z}xms ) { # add server error error class $Errors{NameServerError} = 'ServerError'; $Errors{NameServerErrorMessage} = 'The field does not contain only ASCII letters and numbers.'; } # check if name is duplicated my %DynamicFieldsList = %{ $Self->{DynamicFieldObject}->DynamicFieldList( Valid => 0, ResultType => 'HASH', ) }; %DynamicFieldsList = reverse %DynamicFieldsList; if ( $DynamicFieldsList{ $GetParam{Name} } && $DynamicFieldsList{ $GetParam{Name} } ne $FieldID ) { # add server error class $Errors{NameServerError} = 'ServerError'; $Errors{NameServerErrorMessage} = 'There is another field with the same name.'; } } if ( $GetParam{FieldOrder} ) { # check if field order is numeric and positive if ( $GetParam{FieldOrder} !~ m{\A ( ?: \d )+ \z}xms ) { # add server error error class $Errors{FieldOrderServerError} = 'ServerError'; $Errors{FieldOrderServerErrorMessage} = 'The field must be numeric.'; } } for my $ConfigParam ( qw( ObjectType ObjectTypeName FieldType FieldTypeName DefaultValue ValidID ShowValue ValueMask ) ) { $GetParam{$ConfigParam} = $Self->{ParamObject}->GetParam( Param => $ConfigParam ); } # uncorrectable errors if ( !$GetParam{ValidID} ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Need ValidID", ); } # get dynamic field data my $DynamicFieldData = $Self->{DynamicFieldObject}->DynamicFieldGet( ID => $FieldID, ); # check for valid dynamic field configuration if ( !IsHashRefWithData($DynamicFieldData) ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Could not get data for dynamic field $FieldID", ); } # return to change screen if errors if (%Errors) { return $Self->_ShowScreen( %Param, %Errors, %GetParam, ID => $FieldID, Mode => 'Change', ); } # set specific config my $FieldConfig = { DefaultValue => $GetParam{DefaultValue}, ShowValue => $GetParam{ShowValue}, ValueMask => $GetParam{ValueMask}, }; # update dynamic field (FieldType and ObjectType cannot be changed; use old values) my $UpdateSuccess = $Self->{DynamicFieldObject}->DynamicFieldUpdate( ID => $FieldID, Name => $GetParam{Name}, Label => $GetParam{Label}, FieldOrder => $GetParam{FieldOrder}, FieldType => $DynamicFieldData->{FieldType}, ObjectType => $DynamicFieldData->{ObjectType}, Config => $FieldConfig, ValidID => $GetParam{ValidID}, UserID => $Self->{UserID}, ); if ( !$UpdateSuccess ) { return $Self->{LayoutObject}->ErrorScreen( Message => "Could not update the field $GetParam{Name}", ); } return $Self->{LayoutObject}->Redirect( OP => "Action=AdminDynamicField", ); }
_ChangeAction() is very similar to _AddAction()", but adapted for the update of an existing field instead of creating a new one.
sub _ShowScreen { my ( $Self, %Param ) = @_; $Param{DisplayFieldName} = 'New'; if ( $Param{Mode} eq 'Change' ) { $Param{ShowWarning} = 'ShowWarning'; $Param{DisplayFieldName} = $Param{Name}; } # header my $Output = $Self->{LayoutObject}->Header(); $Output .= $Self->{LayoutObject}->NavigationBar(); # get all fields my $DynamicFieldList = $Self->{DynamicFieldObject}->DynamicFieldListGet( Valid => 0, ); # get the list of order numbers (is already sorted). my @DynamicfieldOrderList; for my $Dynamicfield ( @{$DynamicFieldList} ) { push @DynamicfieldOrderList, $Dynamicfield->{FieldOrder}; } # when adding we need to create an extra order number for the new field if ( $Param{Mode} eq 'Add' ) { # get the last element form the order list and add 1 my $LastOrderNumber = $DynamicfieldOrderList[-1]; $LastOrderNumber++; # add this new order number to the end of the list push @DynamicfieldOrderList, $LastOrderNumber; } my $DynamicFieldOrderSrtg = $Self->{LayoutObject}->BuildSelection( Data => \@DynamicfieldOrderList, Name => 'FieldOrder', SelectedValue => $Param{FieldOrder} || 1, PossibleNone => 0, Class => 'W50pc Validate_Number', ); my %ValidList = $Self->{ValidObject}->ValidList(); # create the Validity select my $ValidityStrg = $Self->{LayoutObject}->BuildSelection( Data => \%ValidList, Name => 'ValidID', SelectedID => $Param{ValidID} || 1, PossibleNone => 0, Translation => 1, Class => 'W50pc', ); # define config field specific settings my $DefaultValue = ( defined $Param{DefaultValue} ? $Param{DefaultValue} : '' ); # create the Show value select my $ShowValueStrg = $Self->{LayoutObject}->BuildSelection( Data => [ 'No', 'Yes' ], Name => 'ShowValue', SelectedValue => $Param{ShowValue} || 'No', PossibleNone => 0, Translation => 1, Class => 'W50pc', ); # generate output $Output .= $Self->{LayoutObject}->Output( TemplateFile => 'AdminDynamicFieldPassword', Data => { %Param, ValidityStrg => $ValidityStrg, DynamicFieldOrderSrtg => $DynamicFieldOrderSrtg, DefaultValue => $DefaultValue, ShowValueStrg => $ShowValueStrg, ValueMask => $Param{ValueMask} || $Self->{DefaultValueMask}, }, ); $Output .= $Self->{LayoutObject}->Footer(); return $Output; } 1;
The _ShowScreen function is used to set and define the HTML elements and blocks from a template to generate the Admin Dialog HTML code.
The template is the place where the HTML code of the dialog is stored.
In this section an Admin Dialog template for the password Dynamic Field is shown and explained.
# -- # AdminDynamicFieldPassword.tt - provides HTML form for AdminDynamicFieldPassword # Copyright (C) 2001-2018 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. # --
This is common header that can be found in common OTRS modules.
<div class="MainBox ARIARoleMain LayoutFixedSidebar SidebarFirst"> <h1>[% Translate("Dynamic Fields") | html %] - [% Translate(Data.ObjectTypeName) | html %]: [% Translate(Data.Mode) | html %] [% Translate(Data.FieldTypeName) | html %] [% Translate("Field") | html %]</h1> <div class="Clear"></div> <div class="SidebarColumn"> <div class="WidgetSimple"> <div class="Header"> <h2>[% Translate("Actions") | html %]</h2> </div> <div class="Content"> <ul class="ActionList"> <li> <a href="[% Env("Baselink") %]Action=AdminDynamicField" class="CallForAction"><span>[% Translate("Go back to overview") | html %]</span></a> </li> </ul> </div> </div> </div>
This is part of the code has the main box and also the actions side bar, no modifications are needed in this section.
<div class="ContentColumn"> <form action="[% Env("CGIHandle") %]" method="post" class="Validate PreventMultipleSubmits"> <input type="hidden" name="Action" value="AdminDynamicFieldPassword" /> <input type="hidden" name="Subaction" value="[% Data.Mode | html %]Action" /> <input type="hidden" name="ObjectType" value="[% Data.ObjectType | html %]" /> <input type="hidden" name="FieldType" value="[% Data.FieldType | html %]" /> <input type="hidden" name="ID" value="[% Data.ID | html %]" />
In this section of the code is defined the right part of the dialog, notice that the value of the Action hidden input must match with the name of the Admin Dialog.
<div class="WidgetSimple"> <div class="Header"> <h2>[% Translate("General") | html %]</h2> </div> <div class="Content"> <div class="LayoutGrid ColumnsWithSpacing"> <div class="Size1of2"> <fieldset class="TableLike"> <label class="Mandatory" for="Name"><span class="Marker">*</span> [% Translate("Name") | html %]:</label> <div class="Field"> <input id="Name" class="W50pc [% Data.NameServerError | html %] [% Data.ShowWarning | html %] Validate_Alphanumeric" type="text" maxlength="200" value="[% Data.Name | html %]" name="Name"/> <div id="NameError" class="TooltipErrorMessage"><p>[% Translate("This field is required, and the value should be alphabetic and numeric characters only.") | html %]</p></div> <div id="NameServerError" class="TooltipErrorMessage"><p>[% Translate(Data.NameServerErrorMessage) | html %]</p></div> <p class="FieldExplanation">[% Translate("Must be unique and only accept alphabetic and numeric characters.") | html %]</p> <p class="Warning Hidden">[% Translate("Changing this value will require manual changes in the system.") | html %]</p> </div> <div class="Clear"></div> <label class="Mandatory" for="Label"><span class="Marker">*</span> [% Translate("Label") | html %]:</label> <div class="Field"> <input id="Label" class="W50pc [% Data.LabelServerError | html %] Validate_Required" type="text" maxlength="200" value="[% Data.Label | html %]" name="Label"/> <div id="LabelError" class="TooltipErrorMessage"><p>[% Translate("This field is required.") | html %]</p></div> <div id="LabelServerError" class="TooltipErrorMessage"><p>[% Translate(Data.LabelServerErrorMessage) | html %]</p></div> <p class="FieldExplanation">[% Translate("This is the name to be shown on the screens where the field is active.") | html %]</p> </div> <div class="Clear"></div> <label class="Mandatory" for="FieldOrder"><span class="Marker">*</span> [% Translate("Field order") | html %]:</label> <div class="Field"> [% Data.DynamicFieldOrderSrtg %] <div id="FieldOrderError" class="TooltipErrorMessage"><p>[% Translate("This field is required and must be numeric.") | html %]</p></div> <div id="FieldOrderServerError" class="TooltipErrorMessage"><p>[% Translate(Data.FieldOrderServerErrorMessage) | html %]</p></div> <p class="FieldExplanation">[% Translate("This is the order in which this field will be shown on the screens where is active.") | html %]</p> </div> <div class="Clear"></div> </fieldset> </div> <div class="Size1of2"> <fieldset class="TableLike"> <label for="ValidID">[% Translate("Validity") | html %]:</label> <div class="Field"> [% Data.ValidityStrg %] </div> <div class="Clear"></div> <div class="SpacingTop"></div> <label for="FieldTypeName">[% Translate("Field type") | html %]:</label> <div class="Field"> <input id="FieldTypeName" readonly="readonly" class="W50pc" type="text" maxlength="200" value="[% Data.FieldTypeName | html %]" name="FieldTypeName"/> <div class="Clear"></div> </div> <div class="SpacingTop"></div> <label for="ObjectTypeName">[% Translate("Object type") | html %]:</label> <div class="Field"> <input id="ObjectTypeName" readonly="readonly" class="W50pc" type="text" maxlength="200" value="[% Data.ObjectTypeName | html %]" name="ObjectTypeName"/> <div class="Clear"></div> </div> </fieldset> </div> </div> </div> </div>
This first widget contains the common form attributes for the Dynamic Fields, for consistency with other Dynamic Fields is recommended to leave this part of the code unchanged.
<div class="WidgetSimple"> <div class="Header"> <h2>[% Translate(Data.FieldTypeName) | html %] [% Translate("Field Settings") | html %]</h2> </div> <div class="Content"> <fieldset class="TableLike"> <label for="DefaultValue">[% Translate("Default value") | html %]:</label> <div class="Field"> <input id="DefaultValue" class="W50pc" type="text" maxlength="200" value="[% Data.DefaultValue | html %]" name="DefaultValue"/> <p class="FieldExplanation">[% Translate("This is the default value for this field.") | html %]</p> </div> <div class="Clear"></div> <label for="ShowValue">[% Translate("Show value") | html %]:</label> <div class="Field"> [% Data.ShowValueStrg %] <p class="FieldExplanation"> [% Translate("To reveal the field value in non edit screens ( e.g. Ticket Zoom Screen )") | html %] </p> </div> <div class="Clear"></div> <label for="ValueMask">[% Translate("Hidden value mask") | html %]:</label> <div class="Field"> <input id="ValueMask" class="W50pc" type="text" maxlength="200" value="[% Data.ValueMask | html %]" name="ValueMask"/> <p class="FieldExplanation"> [% Translate("This is the alternate value to show if Show value is set to \"No\" ( Default: **** ).") | html %] </p> </div> <div class="Clear"></div> </fieldset> </div> </div>
The second widget has the Dynamic Field specific form attributes. This is the place where new attributes cat be set and it could use Java Script and AJAX technologies to make it more easy or friendly for the end user.
<fieldset class="TableLike"> <div class="Field SpacingTop"> <button type="submit" class="Primary" value="[% Translate("Save") | html %]">[% Translate("Save") | html %]</button> [% Translate("or") | html %] <a href="[% Env("Baselink") %]Action=AdminDynamicField">[% Translate("Cancel") | html %]</a> </div> <div class="Clear"></div> </fieldset> </form> </div> </div> [% WRAPPER JSOnDocumentComplete %] <script type="text/javascript">//<![CDATA[ $('.ShowWarning').bind('change keyup', function (Event) { $('p.Warning').removeClass('Hidden'); }); Core.Agent.Admin.DynamicField.ValidationInit(); //]]></script> [% END %]
The final part of the file contains the "Submit" button and the "Cancel" link, as well as other needed Java Script code.
The driver is the Dynamic Field. It contains several functions that are used wide in the OTRS framework. A driver can inherit some functions form base classes, for example TextArea driver inherit most of the functions from Base.pm and BaseText.pm and it only implements the functions that requires different logic or results. Checkbox field driver only inherits from Base.pm as all other functions are very different from any other Base driver.
Please refer to the Perl On-line Documentation (POD) of the module /Kernel/System/DynmicField/Backend.pm to have the list of all attributes and possible return data for each function.
In this section the Password Dynamic Field driver is shown and explained. This driver inherits some functions from Base.pm and BaseText.pm and only implements the functions that needs different results.
# -- # Kernel/System/DynamicField/Driver/Password.pm - Driver for DynamicField Password backend # Copyright (C) 2001-2018 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::System::DynamicField::Driver::Password; use strict; use warnings; use Kernel::System::VariableCheck qw(:all); use Kernel::System::DynamicFieldValue; use base qw(Kernel::System::DynamicField::Driver::BaseText); our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::System::DynamicFieldValue', 'Kernel::System::Main', );
This is the common header that can be found in common OTRS modules. The class/package name is declared via the package keyword. Notice that BaseText is used as base class.
sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # set field behaviors $Self->{Behaviors} = { 'IsACLReducible' => 0, 'IsNotificationEventCondition' => 1, 'IsSortable' => 0, 'IsFiltrable' => 0, 'IsStatsCondition' => 1, 'IsCustomerInterfaceCapable' => 1, }; # get the Dynamic Field Backend custom extensions my $DynamicFieldDriverExtensions = $Kernel::OM->Get('Kernel::Config')->Get('DynamicFields::Extension::Driver::Password'); EXTENSION: for my $ExtensionKey ( sort keys %{$DynamicFieldDriverExtensions} ) { # skip invalid extensions next EXTENSION if !IsHashRefWithData( $DynamicFieldDriverExtensions->{$ExtensionKey} ); # create a extension config shortcut my $Extension = $DynamicFieldDriverExtensions->{$ExtensionKey}; # check if extension has a new module if ( $Extension->{Module} ) { # check if module can be loaded if ( !$Kernel::OM->Get('Kernel::System::Main')->RequireBaseClass( $Extension->{Module} ) ) { die "Can't load dynamic fields backend module" . " $Extension->{Module}! $@"; } } # check if extension contains more behaviors if ( IsHashRefWithData( $Extension->{Behaviors} ) ) { %{ $Self->{Behaviors} } = ( %{ $Self->{Behaviors} }, %{ $Extension->{Behaviors} } ); } } return $Self; }
The constructor new creates a new instance of the class. According to the coding guidelines objects of other classes that are needed in this module should be created here.
It is important to define the behaviors correctly as the field might or might not be used in certain screens, functions that depends on behaviors that are not active for this particular field might not be needed to be implemented.
Drivers are created only by the BankendObject and not directly from any other module.
sub EditFieldRender { my ( $Self, %Param ) = @_; # take config from field config my $FieldConfig = $Param{DynamicFieldConfig}->{Config}; my $FieldName = 'DynamicField_' . $Param{DynamicFieldConfig}->{Name}; my $FieldLabel = $Param{DynamicFieldConfig}->{Label}; my $Value = ''; # set the field value or default if ( $Param{UseDefaultValue} ) { $Value = ( defined $FieldConfig->{DefaultValue} ? $FieldConfig->{DefaultValue} : '' ); } $Value = $Param{Value} if defined $Param{Value}; # extract the dynamic field value form the web request my $FieldValue = $Self->EditFieldValueGet( %Param, ); # set values from ParamObject if present if ( defined $FieldValue ) { $Value = $FieldValue; } # check and set class if necessary my $FieldClass = 'DynamicFieldText W50pc'; if ( defined $Param{Class} && $Param{Class} ne '' ) { $FieldClass .= ' ' . $Param{Class}; } # set field as mandatory $FieldClass .= ' Validate_Required' if $Param{Mandatory}; # set error css class $FieldClass .= ' ServerError' if $Param{ServerError}; my $HTMLString = <<"EOF"; <input type="password" class="$FieldClass" id="$FieldName" name="$FieldName" title="$FieldLabel" value="$Value" /> EOF if ( $Param{Mandatory} ) { my $DivID = $FieldName . 'Error'; # for client side validation $HTMLString .= <<"EOF"; <div id="$DivID" class="TooltipErrorMessage"> <p> \$Text{"This field is required."} </p> </div> EOF } if ( $Param{ServerError} ) { my $ErrorMessage = $Param{ErrorMessage} || 'This field is required.'; my $DivID = $FieldName . 'ServerError'; # for server side validation $HTMLString .= <<"EOF"; <div id="$DivID" class="TooltipErrorMessage"> <p> \$Text{"$ErrorMessage"} </p> </div> EOF } # call EditLabelRender on the common Driver my $LabelString = $Self->EditLabelRender( %Param, DynamicFieldConfig => $Param{DynamicFieldConfig}, Mandatory => $Param{Mandatory} || '0', FieldName => $FieldName, ); my $Data = { Field => $HTMLString, Label => $LabelString, }; return $Data; }
This function is the responsible to create the HTML representation of the field and its label, and is used in the edit screens like "AgentTicketPhone", "AgentTicketNote", etc
sub DisplayValueRender { my ( $Self, %Param ) = @_; # set HTMLOuput as default if not specified if ( !defined $Param{HTMLOutput} ) { $Param{HTMLOutput} = 1; } my $Value; my $Title; # check if field is set to show password or not if ( defined $Param{DynamicFieldConfig}->{Config}->{ShowValue} && $Param{DynamicFieldConfig}->{Config}->{ShowValue} eq 'Yes' ) { # get raw Title and Value strings from field value $Value = defined $Param{Value} ? $Param{Value} : ''; $Title = $Value; } else { # show the mask and not the value $Value = $Param{DynamicFieldConfig}->{Config}->{ValueMask} || ''; $Title = 'The value of this field is hidden.' } # HTMLOuput transformations if ( $Param{HTMLOutput} ) { $Value = $Param{LayoutObject}->Ascii2Html( Text => $Value, Max => $Param{ValueMaxChars} || '', ); $Title = $Param{LayoutObject}->Ascii2Html( Text => $Title, Max => $Param{TitleMaxChars} || '', ); } else { if ( $Param{ValueMaxChars} && length($Value) > $Param{ValueMaxChars} ) { $Value = substr( $Value, 0, $Param{ValueMaxChars} ) . '...'; } if ( $Param{TitleMaxChars} && length($Title) > $Param{TitleMaxChars} ) { $Title = substr( $Title, 0, $Param{TitleMaxChars} ) . '...'; } } # create return structure my $Data = { Value => $Value, Title => $Title, }; return $Data; }
DisplayValueRender() function returns the field value as a plain text as well as its title (both can be translated). for this particular example we are checking if the password should be reveled or display a predefined mask by a configuration parameter in the Dynamic Field.
sub ReadableValueRender { my ( $Self, %Param ) = @_; my $Value; my $Title; # check if field is set to show password or not if ( defined $Param{DynamicFieldConfig}->{Config}->{ShowValue} && $Param{DynamicFieldConfig}->{Config}->{ShowValue} eq 'Yes' ) { # get raw Title and Value strings from field value $Value = $Param{Value} // ''; $Title = $Value; } else { # show the mask and not the value $Value = $Param{DynamicFieldConfig}->{Config}->{ValueMask} || ''; $Title = 'The value of this field is hidden.' } # cut strings if needed if ( $Param{ValueMaxChars} && length($Value) > $Param{ValueMaxChars} ) { $Value = substr( $Value, 0, $Param{ValueMaxChars} ) . '...'; } if ( $Param{TitleMaxChars} && length($Title) > $Param{TitleMaxChars} ) { $Title = substr( $Title, 0, $Param{TitleMaxChars} ) . '...'; } # create return structure my $Data = { Value => $Value, Title => $Title, }; return $Data; }
This function is similar to DisplayValueRender() but is used in places where there is no LayoutObject.
The following are other functions that are might needed if the new Dynamic Field does not inherit from other classes, To see the complete code of this functions please take a look directly into the files Kernel::System::DynamicField::Driver::Base.pm and Kernel::System::DynamicField::Driver::BaseText.pm
sub ValueGet { . . . }
This function retrieves the value from the field on a specified Object. In this case we are returning the first text value, since the field only stores one text value at time.
sub ValueSet { . . . }
ValueSet() is used to store a Dynamic Field value, in this case this field only stores one text type value. Other fields could store more than one value on either ValueText, ValueDateTime, or ValueInt format.
sub ValueDelete { . . . }
This function is used to delete one field value attached to a particular object ID, is used for example if the instance of object is to be deleted, then there is no reason to have the field value stored in the database for that particular object instance.
sub AllValuesDelete { . . . }
AllValuesDelete() function is used to delete all values from a certain Dynamic Field. This function is very useful when a Dynamic Field is going to be deleted.
sub ValueValidate { . . . }
The ValueValidate() function is used to check if the value is consistent to its type.
sub SearchSQLGet { . . . }
This function is used by TicketSearch core module to build the internal query to search for a ticket based on this field as a search parameter.
sub SearchSQLOrderFieldGet { . . . }
The SearchSQLOrderFieldGet is also a helper for TicketSearch module, "$Param{TableAlias}." should be kept and "value_text" could be replaced with "value_date" or "value_int" depending on the field.
sub EditFieldValueGet { . . . }
EditFieldValueGet() is a function used in the edit screens of OTRS and its purpose is to get the value of the field, either form a template like generic agent profile or from a web request. This function gets the web request in the $Param{ParamObject}, that is a copy of the ParamObject of the Frontend Module or screen.
There are two return formats for this function, the normal: that is just the raw value or a structure: that is the pair field name => field value. for example a Date Dynamic Field returns normally the date as string, and if it should return the an structure it returns a pair for each part of the the date in the hash.
The if the result should be a structure then, normally this is used to store its values in a template, like a generic agent profile. For example a date field uses several HTML components to build the field, like the "Used" check-box and selects for year, month, day etc.
sub EditFieldValueValidate { . . . }
This function should provide at least, a method to validate if the field is empty, and return an error if the field is empty and mandatory, but it can also do more validations for other kind of fields, like if the option selected is valid, or if a date should be only in the past etc. It can provide a custom error message also.
sub SearchFieldRender { . . . }
This function is used by ticket search dialog and its similar to EditFieldRander(), but normally on a search screen small changes has to be done for all fields, for this example we use a HTML text input instead of a password input. In other fields like Dropdown field is displayed as a Multiple select in order to let the user search for more than one value at a time.
sub SearchFieldValueGet { . . . }
Very similar to EditFieldValueGet(), but uses a different name prefix, adapted for the search dialog screen.
sub SearchFieldParameterBuild { . . . }
SearchFieldParameterBuild() is used also by the ticket search dialog to set the correct operator and value to do the search on this field. It also returns how the value should be displayed in the used search attributes, in the results page.
sub StatsFieldParameterBuild { . . . }
This function is used by the stats modules, it includes the field definition in the stats format. For fields with fixed values it also includes all this possible values and if they can be translated, take a look to the BaseSelect driver code for an example how to implement those.
sub StatsSearchFieldParameterBuild { . . . }
StatsSearchFieldParameterBuild() is very similar to the SearchFieldParameterBuild(), the difference id that the SearchFieldParameterBuild() gets the value from the search profile and this one gets the value directly from its parameters.
This function is used by statistics module.
sub TemplateValueTypeGet { . . . }
The TemplateValueTypeGet() function is used to know how the Dynamic Field values stored on a profile should be retrieved, as an SCALAR or as an ARRAY, and it also defines the correct name of the field in the profile.
sub RandomValueSet { . . . }
This function is used by otrs.FillDB.pl script to populate the database with some test and random data. The value inserted by this function is not really relevant. The only restriction is that the value must be compatible with the field value type.
sub ObjectMatch { . . . }
Used by the notification modules this function returns 1 if the field is present in the $Param{ObjectAttributes} parameter and if it match the given value.
To illustrate this process a new Dynamic Field functionality extension for the function Foo will be added to the Backend Object as well as in in the Text field driver.
To create this extension we will create 3 files: a Configuration File (XML), to register the modules, a Backend extension (Perl), to define the new function, and a Text field Driver extension (Perl), that implements the new function for Text fields.
File Structure:
$HOME (e. g. /opt/otrs/) | ... |--/Kernel/ | |--/Config/ | | |--/Files/ | | | |DynamicFieldFooExtension.xml ... | |--/System/ | | |--/DynamicField/ | | | FooExtensionBackend.pm | | | |--/Driver/ | | | | |FooExtensionText.pm ...
The configuration files are used to register the extensions for the Backend and Drivers as well as new behaviors for each driver.
If a driver is extended with a new function, the backend will need also an extension for that function.
In this section a configuration file for Foo Extension is shown and explained.
<?xml version="1.0" encoding="utf-8"?> <otrs_config version="1.0" init="Application">
This is the normal header for a configuration file.
<ConfigItem Name="DynamicFields::Extension::Backend###100-Foo" Required="0" Valid="1"> <Description Translatable="1">Dynamic Fields Extension.</Description> <Group>DynamicFieldFooExtension</Group> <SubGroup>DynamicFields::Extension::Registration</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::System::DynamicField::FooExtensionBackend</Item> </Hash> </Setting> </ConfigItem>
This setting registers the extension in the Backend Object. The module will be loaded from Backend as a base class.
<ConfigItem Name="DynamicFields::Extension::Driver::Text###100-Foo" Required="0" Valid="1"> <Description Translatable="1">Dynamic Fields Extension.</Description> <Group>DynamicFieldFooExtension</Group> <SubGroup>DynamicFields::Extension::Registration</SubGroup> <Setting> <Hash> <Item Key="Module">Kernel::System::DynamicField::Driver::FooExtensionText</Item> <Item Key="Behaviors"> <Hash> <Item Key="Foo">1</Item> </Hash> </Item> </Hash> </Setting> </ConfigItem>
This is the registration for a extension in the "Text" Dynamic Field Driver the module will be loaded as a base class in the Driver. Notice also that new behaviors can be specified, this extended behaviors will be added to the behaviors that the Driver has out of the box, therefore a call to HasBehavior() to check for this new behaviors will be totally transparent.
</otrs_config>
Standard closure of a configuration file.
Backend extensions will be loaded transparently into the Backed itself as a base class, all defined object and properties from the Backend will be accessible in the extension.
All new functions defined in the Backend extension should be implemented in a Driver extension.
In this section the Foo extension for Backend is shown and explained. The extension only defines the function Foo().
# -- # Kernel/System/DynamicField/FooExtensionBackend.pm - Extension for DynamicField backend # Copyright (C) 2001-2018 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::System::DynamicField::FooExtensionBackend; use strict; use warnings; use Kernel::System::VariableCheck qw(:all); =head1 NAME Kernel::System::DynamicField::FooExtensionBackend =head1 SYNOPSIS DynamicFields Extension for Backend =head1 PUBLIC INTERFACE =over 4 =cut
This is the common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
=item Foo() Testing function: returns 1 if function is available on a Dynamic Field driver. my $Success = $BackendObject->Foo( DynamicFieldConfig => $DynamicFieldConfig, # complete config of the DynamicField ); Returns: $Success = 1; # or undef =cut sub Foo { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(DynamicFieldConfig)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # check DynamicFieldConfig (general) if ( !IsHashRefWithData( $Param{DynamicFieldConfig} ) ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "The field configuration is invalid", ); return; } # check DynamicFieldConfig (internally) for my $Needed (qw(ID FieldType ObjectType)) { if ( !$Param{DynamicFieldConfig}->{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed in DynamicFieldConfig!", ); return; } } # set the dynamic field specific backend my $DynamicFieldBackend = 'DynamicField' . $Param{DynamicFieldConfig}->{FieldType} . 'Object'; if ( !$Self->{$DynamicFieldBackend} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Backend $Param{DynamicFieldConfig}->{FieldType} is invalid!", ); return; } # verify if function is available return if !$Self->{$DynamicFieldBackend}->can('Foo'); # call HasBehavior on the specific backend return $Self->{$DynamicFieldBackend}->Foo(%Param); }
The function Foo() is only used for test purposes, first it checks the Dynamic Field configuration, then it checks if the Dynamic Field Driver (type) exists and was already loaded. To prevent the function call on a driver where is not defined it first check if the driver can execute the function, then executes the function in the driver passing all parameters.
It is also possible to skip the step that test if the Driver can execute the function, to do that is necessary to implement a mechanism in the Frontend module to require a special behavior on the Dynamic Field, and only after call the function in the Backend object.
Driver extensions will be loaded transparently into the Driver itself as a base class, all defined object and properties from the Driver will be accessible in the extension.
All new functions implemented in the Driver extension should be defined in a Backend extension, as every function is called from the Backend Object.
In this section the Foo extension for Text field driver is shown and explained. The extension only implements the function Foo().
# -- # Kernel/System/DynamicField/Driver/FooExtensionText.pm - Extension for DynamicField Text Driver # Copyright (C) 2001-2018 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::System::DynamicField::Driver::FooExtensionText; use strict; use warnings; =head1 NAME Kernel::System::DynamicField::Driver::FooExtensionText =head1 SYNOPSIS DynamicFields Text Driver Extension =head1 PUBLIC INTERFACE This module extends the public interface of L<Kernel::System::DynamicField::Backend>. Please look there for a detailed reference of the functions. =over 4 =cut
This is the common header that can be found in common OTRS modules. The class/package name is declared via the package keyword.
sub Foo { my ( $Self, %Param ) = @_; return 1; }
The function Foo() has no special logic it is only for testing and it always returns 1.
Please remove these old sections if newer ones were created.
In this module layer you can create dynamic navigation bar items with dynamic content (Name and Description). Navigation Module are located under Kernel/Output/HTML/NavBar*.pm.
Format:
# -- # Kernel/Output/HTML/NavBarABC.pm - shows a navbar item dynamicaly # Copyright (C) 2001-2018 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::NavBarABC; use strict; use warnings; # -- sub new { my ( $Type, %Param ) = @_; [...] return $Self; } # -- sub Run { my ( $Self, %Param ) = @_; my %Return = (); $Return{'0999989'} = { Block => 'ItemPersonal', Description => 'Some Desctipton', Name => 'Text', Image => 'new-message.png', Link => 'Action=AgentMailbox&Subaction=New', AccessKey => 'j', }; return %Return; } # -- 1;
To use this module add the following code to the Kernel/Config.pm and restart your webserver (if you use mod_perl).
[Kernel/Config.pm] # agent interface notification module $Self->{'Frontend::NavBarModule'}->{'99-ABC'} = { Module => 'Kernel::Output::HTML::NavBarABC', };
In this module layer you can create dynamic navigation bar items with dynamic content (Name and Description).
The format is the same as in the Navigation Module.
Just the config setting key is different. To use this module, add the following to the Kernel/Config.pm and restart your webserver (if you use mod_perl).
[Kernel/Config.pm] # customer notifiacation module $Self->{'CustomerFrontend::NavBarModule'}->{'99-ABC'} = { Module => 'Kernel::Output::HTML::NavBarABC', };
PostMaster modules are used during the PostMaster process. There are two kinds of PostMaster modules. PostMasterPre (used after parsing an email) and PostMasterPost (used after an email is processed and in the database) modules.
If you want to create your own postmaster filter, just create your own module. These modules are located under "Kernel/System/PostMaster/Filter/*.pm". For default modules see the admin manual. You just need two functions: new() and Run():
The following is an examplary module to match emails and set X-OTRS-Headers (see doc/X-OTRS-Headers.txt for more info).
Kernel/Config.pm:
# Job Name: 1-Match # (block/ignore all spam email with From: noreply@) $Self->{'PostMaster::PreFilterModule'}->{'1-Example'} = { Module => 'Kernel::System::PostMaster::Filter::Example', Match => { From => 'noreply@', }, Set => { 'X-OTRS-Ignore' => 'yes', }, };
Format:
# -- # Kernel/System/PostMaster/Filter/Example.pm - a postmaster filter # Copyright (C) 2001-2018 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::System::PostMaster::Filter::Example; use strict; use warnings; use vars qw(@ISA $VERSION); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless ($Self, $Type); $Self->{Debug} = $Param{Debug} || 0; # get needed objects for (qw(ConfigObject EncodeObject LogObject DBObject)) { $Self->{$_} = $Param{$_} || die "Got no $_!"; } return $Self; } sub Run { my ( $Self, %Param ) = @_; # get config options my %Config = (); my %Match = (); my %Set = (); if ($Param{JobConfig} && ref($Param{JobConfig}) eq 'HASH') { %Config = %{$Param{JobConfig}}; if ($Config{Match}) { %Match = %{$Config{Match}}; } if ($Config{Set}) { %Set = %{$Config{Set}}; } } # match 'Match => ???' stuff my $Matched = ''; my $MatchedNot = 0; for (sort keys %Match) { if ($Param{GetParam}->{$_} && $Param{GetParam}->{$_} =~ /$Match{$_}/i) { $Matched = $1 || '1'; if ($Self->{Debug} > 1) { $Self->{LogObject}->Log( Priority => 'debug', Message => "'$Param{GetParam}->{$_}' =~ /$Match{$_}/i matched!", ); } } else { $MatchedNot = 1; if ($Self->{Debug} > 1) { $Self->{LogObject}->Log( Priority => 'debug', Message => "'$Param{GetParam}->{$_}' =~ /$Match{$_}/i matched NOT!", ); } } } # should I ignore the incoming mail? if ($Matched && !$MatchedNot) { for (keys %Set) { if ($Set{$_} =~ /\[\*\*\*\]/i) { $Set{$_} = $Matched; } $Param{GetParam}->{$_} = $Set{$_}; $Self->{LogObject}->Log( Priority => 'notice', Message => "Set param '$_' to '$Set{$_}' (Message-ID: $Param{GetParam}->{'Message-ID'}) ", ); } } return 1; } 1;
The following image shows you the email processing flow.
To add links in the ticket menu, just use ticket menu modules.
If you want to create your own ticket menu link, just create your own module. These modules are located under "Kernel/Output/HTML/TicketMenu*.pm". For default modules see the admin manual. You just need two functions: new() and Run():
The following example shows you how to show a lock or a unlock ticket link.
Kernel/Config.pm:
# for ticket zoom menu $Self->{'Ticket::Frontend::MenuModule'}->{'100-Lock'} = { Action => 'AgentTicketLock', Module => 'Kernel::Output::HTML::TicketMenuLock', Name => 'Lock' }; # for ticket preview menu $Self->{'Ticket::Frontend::PreMenuModule'}->{'100-Lock'} = { Action => 'AgentTicketLock', Module => 'Kernel::Output::HTML::TicketMenuLock', Name => 'Lock' };
Format:
# -- # Kernel/Output/HTML/TicketMenuLock.pm # Copyright (C) 2001-2018 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::TicketMenuLock; use strict; use warnings; use vars qw(@ISA $VERSION); # -- sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless ($Self, $Type); # get needed objects for (qw(ConfigObject LogObject DBObject LayoutObject UserID TicketObject)) { $Self->{$_} = $Param{$_} || die "Got no $_!"; } return $Self; } # -- sub Run { my ( $Self, %Param ) = @_; # check needed stuff if (!$Param{Ticket}) { $Self->{LogObject}->Log(Priority => 'error', Message => 'Need Ticket!'); return; } # check permission if ($Self->{TicketObject}->LockIsTicketLocked(TicketID => $Param{TicketID})) { my $AccessOk = $Self->{TicketObject}->OwnerCheck( TicketID => $Param{TicketID}, OwnerID => $Self->{UserID}, ); if (!$AccessOk) { return $Param{Counter}; } } $Self->{LayoutObject}->Block( Name => 'Menu', Data => { }, ); if ($Param{Counter}) { $Self->{LayoutObject}->Block( Name => 'MenuItemSplit', Data => { }, ); } if ($Param{Ticket}->{Lock} eq 'lock') { $Self->{LayoutObject}->Block( Name => 'MenuItem', Data => { %{$Param{Config}}, %{$Param{Ticket}}, %Param, Name => 'Unlock', Description => 'Unlock to give it back to the queue!', Link => 'Action=AgentTicketLock&Subaction=Unlock&TicketID=$QData{"TicketID"}', }, ); } else { $Self->{LayoutObject}->Block( Name => 'MenuItem', Data => { %{$Param{Config}}, %Param, Name => 'Lock', Description => 'Lock it to work on it!', Link => 'Action=AgentTicketLock&Subaction=Lock&TicketID=$QData{"TicketID"}', }, ); } $Param{Counter}++; return $Param{Counter}; } # -- 1;