LinkObject API changesWith every new minor or major version of OTRS, you need to port your package(s) and make sure they still work with the OTRS API.
This section lists changes that you need to examine when porting your package from OTRS 5 to 6.
In OTRS 6, a new module for date and time calculation was added:
Kernel::System::DateTime. The module Kernel::System::Time is now
deprecated and should not be used for new code anymore.
The main advantage of the new Kernel::System::DateTime module is the support for real
time zones like Europe/Berlin instead of time offsets in hours like
+2. Note that also the old Kernel::System::Time module has been
improved to support time zones. Time offsets have been completely dropped. This means that any code that
uses time offsets for calculations has to be ported to use the new DateTime module
instead. Code that doesn't fiddle around with time offsets itself can be left untouched in most cases. You
just have to make sure that upon creation of a Kernel::System::Time object a valid time
zone will be given.
Here's an example for porting time offset code to time zones:
my $TimeObject = $Kernel::OM->Get('Kernel::System::Time'); # Assume a time offset of 0 for this time object
my $SystemTime = $TimeObject->TimeStamp2SystemTime( String => '2004-08-14 22:45:00' );
my $UserTimeZone = '+2'; # normally retrieved via config or param
my $UserSystemTime = $SystemTime + $UserTimeZone * 3600;
my $UserTimeStamp = $TimeObject->SystemTime2TimeStamp( SystemTime => $UserSystemTime );
Code using the new Kernel::System::DateTime module:
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); # This implicitly sets the configured OTRS time zone
my $UserTimeZone = 'Europe/Berlin'; # normally retrieved via config or param
$DateTimeObject->ToTimeZone( TimeZone => $UserTimeZone );
my $SystemTime = $DateTimeObject->ToEpoch(); # note that the epoch is independent from the time zone, it's always calculated for UTC
my $UserTimeStamp = $DateTimeObject->ToString();
Please note that the returned time values with the new Get() function in the Kernel::System::DateTime module
are without leading zero instead of the old SystemTime2Date() function in the Kernel::System::Time module.
In the new Kernel::System::DateTime module the function Format() returns the date/time as string formatted
according to the given format.
For OTRS 6, a multi attachment upload functionality was added. To implement the multi attachment upload in other extensions it is necessary to remove the attachment part from the template file, also the JSOnDocumentComplete parts (AttachmentDelete and AttachmentUpload). Please keep in mind, in some cases the JavaScript parts are already outsourced in Core.Agent.XXX files.
Please note that this is currently only applicable for places where it actually makes sense to have the possibility to upload multiple files (like AgentTicketPhone, AgentTicketCompose, etc.). This is not usable out of the box for admin screens.
To include the new multi attachment upload in the template, replace the existing input type="file" with the following code in your .tt template file:
<label>[% Translate("Attachments") | html %]:</label>
<div class="Field">
[% INCLUDE "FormElements/AttachmentList.tt" %]
</div>
<div class="Clear"></div>
It is also necessary to remove the IsUpload variable and all other IsUpload parts from the Perl module. Code parts like following are not needed anymore:
my $IsUpload = ( $ParamObject->GetParam( Param => 'AttachmentUpload' ) ? 1 : 0 );
Additional to that, the Attachment Layout Block needs to be replaced:
$LayoutObject->Block(
Name => 'Attachment',
Data => $Attachment,
);
Replace it with this code:
push @{ $Param{AttachmentList} }, $Attachment;
If the module where you want to integrate multi upload supports standard templates, make sure to add a section to have a human readable file size format right after the attachments of the selected template have been loaded (see e.g. AgentTicketPhone for reference):
for my $Attachment (@TicketAttachments) {
$Attachment->{Filesize} = $LayoutObject->HumanReadableDataSize(
Size => $Attachment->{Filesize},
);
}
When adding selenium unit tests for the modules you ported, please take a look at Selenium/Agent/MultiAttachmentUpload.t for reference.
In OTRS 6, all admin modules should have a breadcrumb. The breadcrumb only needs to be added on the .tt template file and should be placed right after the h1 headline on top of the file. Additionally, the headline should receive the class InvisibleText to make it only visible for screen readers.
<div class="MainBox ARIARoleMain LayoutFixedSidebar SidebarFirst">
<h1 class="InvisibleText">[% Translate("Name of your module") | html %]</h1>
[% BreadcrumbPath = [
{
Name => Translate('Name of your module'),
},
]
%]
[% INCLUDE "Breadcrumb.tt" Path = BreadcrumbPath %]
...
Please make sure to add the correct breadcrumb for all levels of your admin module (e.g. Subactions):
[% BreadcrumbPath = [
{
Name => Translate('Module Home Screen'),
Link => Env("Action"),
},
{
Name => Translate("Some Subaction"),
},
]
%]
[% INCLUDE "Breadcrumb.tt" Path = BreadcrumbPath %]
Admin modules in OTRS 6 should not only have a Save button, but also a Save and finish button. Save should leave the user on the same edit page after saving, Save and finish should lead back to the overview of the entity the user is currently working on. Please see existing OTRS admin screens for reference.
<div class="Field SpacingTop SaveButtons">
<button class="Primary CallForAction" id="SubmitAndContinue" type="submit" value="[% Translate("Save") | html %]"><span>[% Translate("Save") | html %]</span></button>
[% Translate("or") | html %]
<button class="Primary CallForAction" id="Submit" type="submit" value="[% Translate("Save") | html %]"><span>[% Translate("Save and finish") | html %]</span></button>
[% Translate("or") | html %]
<a href="[% Env("Baselink") %]Action=[% Env("Action") %]"><span>[% Translate("Cancel") | html %]</span></a>
</div>
OTRS 6 uses a new XML configuration file format
and the location of configuration files moved
from Kernel/Config/Files to Kernel/Config/Files/XML.
To convert existing XML configuration files to the new format and location, you can use the following
tool that is part of the OTRS framework:
bin/otrs.Console.pl Dev::Tools::Migrate::ConfigXMLStructure --source-directory Kernel/Config/Files
Migrating configuration XML files...
Kernel/Config/Files/Calendar.xml -> Kernel/Config/Files/XML/Calendar.xml... Done.
Kernel/Config/Files/CloudServices.xml -> Kernel/Config/Files/XML/CloudServices.xml... Done.
Kernel/Config/Files/Daemon.xml -> Kernel/Config/Files/XML/Daemon.xml... Done.
Kernel/Config/Files/Framework.xml -> Kernel/Config/Files/XML/Framework.xml... Done.
Kernel/Config/Files/GenericInterface.xml -> Kernel/Config/Files/XML/GenericInterface.xml... Done.
Kernel/Config/Files/ProcessManagement.xml -> Kernel/Config/Files/XML/ProcessManagement.xml... Done.
Kernel/Config/Files/Ticket.xml -> Kernel/Config/Files/XML/Ticket.xml... Done.
Done.
OTRS 6 speeds up configuration file loading by dropping support for the old configuration format (1)
that just used sequential Perl code and had to be run by eval
and instead enforcing the new package-based format (1.1) for Perl configuration files.
OTRS 6+ can only load files with this format, please make sure to convert any custom developments to it
(see Kernel/Config/Files/ZZZ*.pm for examples). Every Perl configuration file
needs to contain a package with a Load() method.
In the past, Perl configuration files were sometimes misused as an autoload mechanism
to override code in existing packages. This is not necessary any more as OTRS 6 features a dedicated
Autoload mechanism. Please see Kernel/Autoload/Test.pm for a demonstration
on how to use this mechanism to add a method in an existing file.
The structure of POD in Perl files was slightly improved and should be adapted in all files. POD is now also enforced to be syntactically correct.
What was previously called SYNOPSIS is now changed to DESCRIPTION, as
a synopsis typically provides a few popular code usage examples and not a description of the module itself.
An additional synopsis can be provided, of course. Here's how an example:
=head1 NAME
Kernel::System::ObjectManager - Central singleton manager and object instance generator
=head1 SYNOPSIS
# In toplevel scripts only!
local $Kernel::OM = Kernel::System::ObjectManager->new();
# Everywhere: get a singleton instance (and create it, if needed).
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# Remove singleton objects and all their dependencies.
$Kernel::OM->ObjectsDiscard(
Objects => ['Kernel::System::Ticket', 'Kernel::System::Queue'],
);
=head1 DESCRIPTION
The ObjectManager is the central place to create and access singleton OTRS objects (via C<L</Get()>>)
as well as create regular (unmanaged) object instances (via C<L</Create()>>).
In case the DESCRIPTION does not add any value to the line in the NAME
section, it should be rewritten or removed altogether.
The second important change is that functions are now documented as =head2 instead of the
previously used =item.
=head2 Get()
Retrieves a singleton object, and if it not yet exists, implicitly creates one for you.
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# On the second call, this returns the same ConfigObject as above.
my $ConfigObject2 = $Kernel::OM->Get('Kernel::Config');
=cut
sub Get { ... }
These changes lead to an improved online API documentation as can be seen in the ObjectManager documentation for OTRS 5 and OTRS 6.
With OTRS 6, all JavaScript - especially located in JSOnDocumentComplete blocks - is
removed from template files and moved to JavaScript files instead. Only in very rare conditions JavaScript
needs to be placed within template files. For all other occurrences, place the JS code in module-specific
JavaScript files. An Init() method within such a JavaScript file is executed
automatically on file load (for the initialization of event bindings etc.) if you register the JavaScript
file at the OTRS application. This is done by executing Core.Init.RegisterNamespace(TargetNS,
'APP_MODULE'); at the end of the namespace declaration within the JavaScript file.
Along with the refactoring of the JavaScript within template files (see above), the template files for the
rich text editor (RichTextEditor.tt and CustomerRichTextEditor.tt)
were removed as they are no longer necessary.
Typically, these template files were included in the module-specific template files within a block:
[% RenderBlockStart("RichText") %]
[% InsertTemplate("RichTextEditor.tt") %]
[% RenderBlockEnd("RichText") %]
This is no longer needed and can be removed. Instead of calling this block from the Perl module, it is now necessary to set the needed rich text parameters there. Instead of:
$LayoutObject->Block(
Name => 'RichText',
Data => \%Param,
);
you now have to call:
$LayoutObject->SetRichTextParameters(
Data => \%Param,
);
Same rule applies for customer interface. Remove RichText blocks from CustomerRichTextEditor.tt and
apply following code instead:
$LayoutObject->CustomerSetRichTextParameters(
Data => \%Param,
);
Adding translatable strings in JavaScript was quite difficult in OTRS. The string had to be translated in Perl or in the template and then sent to the JavaScript function. With OTRS 6, translation of strings is possible directly in the JavaScript file. All other workarounds, especially blocks in the templates only for translating strings, should be removed.
Instead, the new JavaScript translation namespace Core.Language should be used to
translate strings directly in the JS file:
Core.Language.Translate('The string to translate');
It is also possible to handover JS variables to be replaced in the string directly:
Core.Language.Translate('The %s to %s', 'string', 'translate');
Every %s is replaced by the variable given as extra parameter. The number of parameters
is not limited.
To achieve template files without JavaScript code, some other workarounds had to be replaced with an appropriate solution. Besides translations, also the handover of data from Perl to JavaScript has been a problem in OTRS. The workaround was to add a JavaScript block in the template in which JavaScript variables were declared and filled with template tags based on data handed over from Perl to the template.
The handover process of data from Perl to JavaScript is now much easier in OTRS 6. To send specific data as variable from Perl to JavaScript, one only has to call a function on Perl-side. The data is than automatically available in JavaScript.
In Perl you only have to call:
$Self->{LayoutObject}->AddJSData(
Key => 'KeyToBeAvailableInJS',
Value => $YourData,
);
The Value parameter is automatically converted to a JSON object and can also contain
complex data.
In JavaScript you can get the data with:
Core.Config.Get('KeyToBeAvailableInJS');
This replaces all workarounds which need to be removed when porting a module to OTRS 6, because JavaScript in template files is now only allowed in very rare conditions (see above).
OTRS 6 exposes new JavaScript template API via Core.Template class. You can use it in
your JavaScript code in a similar way as you use TemplateToolkit from Perl code.
Here's an example for porting existing jQuery based code to new template API:
var DivID = 'MyDiv',
DivText = 'Hello, world!';
$('<div />').addClass('CSSClass')
.attr('id', DivID)
.text(DivText)
.appendTo('body');
First, make sure to create a new template file under
Kernel/Output/JavaScript/Templates/Standard folder. In doing this, you should keep
following in mind:
Create a subfolder with name of your Module.
You may reuse any existing subfolder structure but only if it makes sense for your component
(e.g. Agent/MyModule/ or Agent/Admin/MyModule/).
Use .html.tmpl as extension for template file.
Name templates succinctly and clearly in order to avoid confusion (i.e. good:
Agent/MyModule/SettingsDialog.html.tmpl, bad:
Agent/SettingsDialogTemplate.html.tmpl).
Then, add your HTML to the template file, making sure to use placeholders for any variables you might need:
<div id="{{ DivID }}" class="CSSClass">
{{ DivText | Translate }}
</div>
Then, just get rendered HTML by calling Core.Template.Render method with template path
(without extension) and object containing variables for replacement:
var DivHTML = Core.Template.Render('Agent/MyModule/SettingsDialog', {
DivID: 'MyDiv',
DivText: 'Hello, world!'
});
$(DivHTML).appendTo('body');
Internally, Core.Template uses Nunjucks engine for parsing templates. Essentially, any
valid Nunjucks syntax is supported, please see
their documentation for more
information.
Here are some tips:
You can use | Translate filter for string translation to current language.
All {{ VarName }} variable outputs are HTML escaped by default. If you need
to output some existing HTML, please use | safe filter to bypass escaping.
Use | urlencode for encoding URL parameters.
Complex structures in replacement object are supported, so feel free to pass arrays or hashes
and iterate over them right from template. For example, look at {% for %}
syntax in Nunjucks
documentation.
Before OTRS 6, user permissions were stored in the session and passed to the LayoutObject as
attributes, which were then in turn accessed to determine user permissions like if
($LayoutObject->{'UserIsGroup[admin]'}) { ... }.
With OTRS 6, permissions are no longer stored in the session and also not passed to the
LayoutObject. Please switch your code to calling PermissionCheck() on
Kernel::System::Group (for agents) or Kernel::System::CustomerGroup (for
customers). Here's an example:
my $HasPermission = $Kernel::OM->Get('Kernel::System::Group')->PermissionCheck(
UserID => $UserID,
GroupName => $GroupName,
Type => 'move_into',
);
For OTRS 6, all extensions need to be checked and ported from $Ticket{SolutionTime} to
$Ticket{Closed} if TicketGet() is called with the
Extended parameter (see
bug#11872).
Additionally, the database column ticket.create_time_unix was removed, and likewise the
value CreateTimeUnix from the TicketGet() result data.
Please use the value Created (database column ticket.create_time) instead.
In OTRS 6, old ticket-specific LinkObject events have been dropped:
TicketSlaveLinkAdd
TicketSlaveLinkDelete
TicketMasterLinkDelete
Any event handlers listening on these events should be ported to two new events instead:
LinkObjectLinkAdd
LinkObjectLinkDelete
These new events will be triggered any time a link is added or deleted by LinkObject,
regardless of the object type. Data parameter will contain all information your event
handlers might need for further processing, e.g.:
SourceObject
Name of the link source object (e.g. Ticket).
SourceKey
Key of the link source object (e.g. TicketID).
TargetObject
Name of the link target object (e.g. FAQItem).
TargetKey
Key of the link target object (e.g. FAQItemID).
Type
Type of the link (e.g. ParentChild).
State
State of the link (Valid or Temporary).
With these new events in place, any events specific for custom LinkObject module
implementations can be dropped, and all event handlers ported to use them instead. Since source and
target object names are provided in the event itself, it would be trivial to make them run only in
specific situations.
To register your event handler for these new events, make sure to add a registration in the configuration, for example:
<!-- OLD STYLE -->
<ConfigItem Name="LinkObject::EventModulePost###1000-SampleModule" Required="0" Valid="1">
<Description Translatable="1">Event handler for sample link object module.</Description>
<Group>Framework</Group>
<SubGroup>Core::Event::Package</SubGroup>
<Setting>
<Hash>
<Item Key="Module">Kernel::System::LinkObject::Event::SampleModule</Item>
<Item Key="Event">(LinkObjectLinkAdd|LinkObjectLinkDelete)</Item>
<Item Key="Transaction">1</Item>
</Hash>
</Setting>
</ConfigItem>
<!-- NEW STYLE -->
<Setting Name="LinkObject::EventModulePost###1000-SampleModule" Required="0" Valid="1">
<Description Translatable="1">Event handler for sample link object module.</Description>
<Navigation>Core::Event::Package</Navigation>
<Value>
<Hash>
<Item Key="Module">Kernel::System::LinkObject::Event::SampleModule</Item>
<Item Key="Event">(LinkObjectLinkAdd|LinkObjectLinkDelete)</Item>
<Item Key="Transaction">1</Item>
</Hash>
</Value>
</Setting>
In OTRS 6, changes to Article API have been made, in preparations for new Omni Channel infrastructure.
Article object now provides top-level article functions that do not involve back-end related data.
Following methods related to articles have been moved to
Kernel::System::Ticket::Article object:
ArticleFlagSet()
ArticleFlagDelete()
ArticleFlagGet()
ArticleFlagsOfTicketGet()
ArticleAccountedTimeGet()
ArticleAccountedTimeDelete()
ArticleSenderTypeList()
ArticleSenderTypeLookup()
SearchStringStopWordsFind()
SearchStringStopWordsUsageWarningActive()
If you are referencing any of these methods via Kernel::System::Ticket object in your
code, please switch to Article object and use it instead. For example:
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();
New ArticleList() method is now provided by the article object, and can be used for
article listing and locating. This method implements filters and article numbering and returns article
meta data only as an ordered list. For example:
my @Articles = $ArticleObject->ArticleList(
TicketID => 123,
CommunicationChannel => 'Email', # optional, to limit to a certain CommunicationChannel
SenderType => 'customer', # optional, to limit to a certain article SenderType
IsVisibleForCustomer => 1, # optional, to limit to a certain visibility
OnlyFirst => 1, # optional, only return first match, or
OnlyLast => 1, # optional, only return last match
);
Following methods related to articles have been dropped all-together. If you are using any of them in your code, please evaluate possibility of alternatives.
ArticleFirstArticle() (use
ArticleList( OnlyFirst => 1) instead)
ArticleLastCustomerArticle() (use
ArticleList( SenderType => 'customer', OnlyLast => 1) or similar)
ArticleCount() (use ArticleList() instead)
ArticlePage() (reimplemented in AgentTicketZoom)
ArticleTypeList()
ArticleTypeLookup()
ArticleIndex() (use ArticleList() instead)
ArticleContentIndex()
To work with article data please use new article backend API. To get correct backend object for an article, please use:
BackendForArticle(%Article)
BackendForChannel( ChannelName => $ChannelName )
BackendForArticle() returns the correct back end for a given article, or the invalid
back end, so that you can always expect a back end object instance that can be used for chain-calling.
my $ArticleBackendObject = $ArticleObject->BackendForArticle( TicketID => 42, ArticleID => 123 );
BackendForChannel() returns the correct back end for a given communication channel.
my $ArticleBackendObject = $ArticleObject->BackendForChannel( ChannelName => 'Email' );
All other article data and related methods have been moved to separate backends. Every communication channel now has a dedicated backend API that handles article data and can be used to manipulate it.
OTRS 6 Free ships with some default channels and corresponding backends:
Email (equivalent to old email article types)
Phone (equivalent to old phone article types)
Internal (equivalent to old note article types)
Chat (equivalent to old chat article types)
While chat article backend is available in OTRS 6 Free, it is only utilized when system has a valid OTRS Business Solution™ installed.
Article data manipulation can be managed via following backend methods:
ArticleCreate()
ArticleUpdate()
ArticleGet()
ArticleDelete()
All of these methods have dropped article type parameter, which must be substituted for
SenderType and IsVisibleForCustomer parameter combination. In
addition, all these methods now also require TicketID and UserID
parameters.
Since changes in article API are system-wide, any code using the old API must be ported for OTRS 6. This includes any web service definitions which leverage these methods directly via GenericInterface for example. They will need to be re-assessed and adapted to provide all required parameters to the new API during requests and manage subsequent responses in new format.
Please note that returning hash of ArticleGet() has changed, and some things (like
ticket data) might be missing. Utilize parameters like DynamicFields => 1 and
RealNames => 1 to get more information.
In addition, attachment data is not returned any more, please use combination of following methods from the article backends:
ArticleAttachmentIndex()
ArticleAttachment()
Note that ArticleAttachmentIndex() parameters and behavior has changed. Instead of
old strip parameter use combination of new ExcludePlainText,
ExcludeHTMLBody and ExcludeInline.
As an example, here is how to get all article and attachment data in the same hash:
my @Articles = $ArticleObject->ArticleList(
TicketID => $TicketID,
);
ARTICLE:
for my $Article (@Articles) {
# Make sure to retrieve backend object for this specific article.
my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$Article} );
my %ArticleData = $ArticleBackendObject->ArticleGet(
%{$Article},
DynamicFields => 1,
UserID => $UserID,
);
$Article = \%ArticleData;
# Get attachment index (without attachments).
my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex(
ArticleID => $Article->{ArticleID},
UserID => $UserID,
);
next ARTICLE if !%AtmIndex;
my @Attachments;
ATTACHMENT:
for my $FileID ( sort keys %AtmIndex ) {
my %Attachment = $ArticleBackendObject->ArticleAttachment(
ArticleID => $Article->{ArticleID},
FileID => $FileID,
UserID => $UserID,
);
next ATTACHMENT if !%Attachment;
$Attachment{FileID} = $FileID;
$Attachment{Content} = encode_base64( $Attachment{Content} );
push @Attachments, \%Attachment;
}
# Include attachment data in article hash.
$Article->{Atms} = \@Attachments;
}
To make article indexing more generic, article backends now provide information necessary for properly
indexing article data. Index will be created similar to old StaticDB mechanism and
stored in a dedicated article search table.
Since now every article backend can provide search on arbitrary number of article fields, use
BackendSearchableFieldsGet() method to get information about them. This data can
also be used for forming requests to TicketSearch() method. Coincidentally, some
TicketSearch() parameters have changed their name to also include article backend
information, for example:
| Old parameter | New parameter |
|---|---|
From | MIMEBase_From |
To | MIMEBase_To |
Cc | MIMEBase_Cc |
Subject | MIMEBase_Subject |
Body | MIMEBase_Body |
AttachmentName | MIMEBase_AttachmentName |
Additionally, article search indexing will be done in an async call now, in order to off-load index
calculation to a separate task. While this is fine for production systems, it might create new problems
in certain situations, e.g. unit tests. If you are manually creating articles in your unit test, but
expect it to be searchable immediately after created, make sure to manually call the new
ArticleSearchIndexBuild() method on article object.
Note that in OTRS 6 SysConfig API was changed, so you should check if the methods are still existing.
For example, ConfigItemUpdate() is removed. To replace it you should use combination
of the following methods:
SettingLock()
SettingUpdate()
ConfigurationDeploy()
In case that you want to update a configuration setting during a CodeInstall section of a package, you could use
SettingsSet(). It does all previously mentioned steps and it can be used for multiple settings at once.
Do not use SettingSet() in the SysConfig GUI itself.
my $Success = $SysConfigObject->SettingsSet(
UserID => 1, # (required) UserID
Comments => 'Deployment comment', # (optional) Comment
Settings => [ # (required) List of settings to update.
{
Name => 'Setting::Name', # (required)
EffectiveValue => 'Value', # (optional)
IsValid => 1, # (optional)
UserModificationActive => 1, # (optional)
},
...
],
);
Note that LinkObject was slightly modified in the OTRS 6 and methods
LinkList() and LinkKeyList() might return different
result if Direction parameter is used. Consider changing Direction.
Old code:
my $LinkList = $LinkObject->LinkList(
Object => 'Ticket',
Key => '321',
Object2 => 'FAQ',
State => 'Valid',
Type => 'ParentChild',
Direction => 'Target',
UserID => 1,
);
New code:
my $LinkList = $LinkObject->LinkList(
Object => 'Ticket',
Key => '321',
Object2 => 'FAQ',
State => 'Valid',
Type => 'ParentChild',
Direction => 'Source',
UserID => 1,
);
As part of email handling improvements for OTRS 6, a new logging mechanism was added to OTRS 6, exclusively used for incoming and outgoing communications. All PostMaster filters were enriched with this new Communication Log API, which means any additional filters coming with packages should also leverage the new log feature.
If your package implements additional PostMaster filters, make sure to get acquainted with
API usage instructions. Also, you can get an example
of how to implement this logging mechanism by looking the code in the
Kernel::System::PostMaster::NewTicket.
As part of email handling improvements for OTRS 6, all emails are now sent asynchronously, that means they are saved in a queue for future processing.
To the unit tests that depend on emails continue to work properly is necessary to force the processing of the email queue.
Make sure to start with a clean queue:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
$MailQueueObject->Delete();
If for some reason you can't clean completely the queue, e.g. selenium unit tests, just delete the items created during the tests:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my %MailQueueCurrentItems = map { $_->{ID} => $_ } @{ $MailQueueObject->List() || [] };
my $Items = $MailQueueObject->List();
MAIL_QUEUE_ITEM:
for my $Item ( @{$Items} ) {
next MAIL_QUEUE_ITEM if $MailQueueCurrentItems{ $Item->{ID} };
$MailQueueObject->Delete(
ID => $Item->{ID},
);
}
Process the queue after the code that you expect to send emails:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my $QueueItems = $MailQueueObject->List();
for my $Item ( @{$QueueItems} ) {
$MailQueueObject->Send( %{$Item} );
}
Or process only the ones created during the tests:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my $QueueItems = $MailQueueObject->List();
MAIL_QUEUE_ITEM:
for my $Item ( @{$QueueItems} ) {
next MAIL_QUEUE_ITEM if $MailQueueCurrentItems{ $Item->{ID} };
$MailQueueObject->Send( %{$Item} );
}
Depending on your case, you may need to clean the queue after or before processing it.
The widgets in the ticket zoom screen have been improved to work in a more generic way. With OTRS 6, it is now possible to add new widgets for the ticket zoom screen via the SysConfig. It is possible to configure the used module, the location of the widget (e.g. Sidebar) and if the content should be loaded synchronously (default) or via AJAX.
Here is an example configuration for the default widgets:
<Setting Name="Ticket::Frontend::AgentTicketZoom###Widgets###0100-TicketInformation" Required="0" Valid="1">
<Description Translatable="1">AgentTicketZoom widget that displays ticket data in the side bar.</Description>
<Navigation>Frontend::Agent::View::TicketZoom</Navigation>
<Value>
<Hash>
<Item Key="Module">Kernel::Output::HTML::TicketZoom::TicketInformation</Item>
<Item Key="Location">Sidebar</Item>
</Hash>
</Value>
</Setting>
<Setting Name="Ticket::Frontend::AgentTicketZoom###Widgets###0200-CustomerInformation" Required="0" Valid="1">
<Description Translatable="1">AgentTicketZoom widget that displays customer information for the ticket in the side bar.</Description>
<Navigation>Frontend::Agent::View::TicketZoom</Navigation>
<Value>
<Hash>
<Item Key="Module">Kernel::Output::HTML::TicketZoom::CustomerInformation</Item>
<Item Key="Location">Sidebar</Item>
<Item Key="Async">1</Item>
</Hash>
</Value>
</Setting>
With this change, the template blocks in the widget code have been removed, so you should check if you use
the old widget blocks in some output filters via
Frontend::Template::GenerateBlockHooks functionality, and implement it in the new
fashion.
This section lists changes that you need to examine when porting your package from OTRS 4 to 5.
In OTRS 5, Kernel/Output/HTML was restructured. All Perl modules (except
Layout.pm) were moved to subdirectories (one for every module layer). Template (theme)
files were also moved from Kernel/Output/HTML/Standard to
Kernel/Output/HTML/Templates/Standard. Please perform this migration also in your code.
With OTRS 5 there is no support for pre output filters any more. These filters changed
the template content before it was parsed, and that could potentially lead to bad performance issues because
the templates could not be cached any more and had to be parsed and compiled every time.
Just switch from pre to post output filters. To translate content, you
can run $LayoutObject->Translate() directly. If you need other template features, just define a
small template file for your output filter and use it to render your content before injecting it into the
main data. It can also be helpful to use jQuery DOM operations to reorder/replace content on the screen in
some cases instead of using regular expressions. In this case you would inject the new code somewhere in the
page as invisible content (e. g. with the class Hidden), and then move it with jQuery to
the correct location in the DOM and show it.
To make using post output filters easier, there is also a new mechanism to request HTML comment hooks for certain templates/blocks. You can add in your module config XML like:
<ConfigItem
Name="Frontend::Template::GenerateBlockHooks###100-OTRSBusiness-ContactWithData"
Required="1" Valid="1">
<Description Translatable="1">Generate HTML comment hooks for
the specified blocks so that filters can use them.</Description>
<Group>OTRSBusiness</Group>
<SubGroup>Core</SubGroup>
<Setting>
<Hash>
<Item Key="AgentTicketZoom">
<Array>
<Item>CustomerTable</Item>
</Array>
</Item>
</Hash>
</Setting>
</ConfigItem>
This will cause the block CustomerTable in AgentTicketZoom.tt to be
wrapped in HTML comments each time it is rendered:
<!--HookStartCustomerTable-->
... block output ...
<!--HookEndCustomerTable-->
With this mechanism every package can request just the block hooks it needs, and they are consistently rendered. These HTML comments can then be used in your output filter for easy regular expression matching.
Support for IE 8 and 9
was
dropped. You can remove any workarounds in your code for these platforms, as well as any old
<CSS_IE7> or <CSS_IE8> loader tags that might still lurk
in your XML config files.
The operation TicketGet() returns dynamic field data from ticket and articles differently than
in OTRS 4. Now they are cleanly separated from the rest of the static ticket and article fields - they are
now grouped in a list called DynamicField. Please adapt any applications using this
operation accordingly.
# changed from:
Ticket => [
{
TicketNumber => '20101027000001',
Title => 'some title',
...
DynamicField_X => 'value_x',
},
]
# to:
Ticket => [
{
TicketNumber => '20101027000001',
Title => 'some title',
...
DynamicField => [
{
Name => 'some name',
Value => 'some value',
},
],
},
]
The new statistics GUI provides a preview for the current configuration. This must be implemented in the
statistic modules and usually returns fake / random data for speed reasons. So for any dynamic (matrix)
statistic that provides the method GetStatElement() you should also add a method
GetStatElementPreview(), and for every dynamic (table) statistic that provides
GetStatTable() you should accordingly add GetStatTablePreview(). Otherwise the
preview in the new statistics GUI will not work for your statistics. You can find example implementations in
the default OTRS statistics.
Until OTRS 5, the Perl module PDF::API2 was not present on all systems. Therefore a
fallback HTML print mode existed. With OTRS 5, the module is now bundled and HTML print was dropped.
$LayoutObject->PrintHeader() and PrintFooter() are not available any more. Please
remove the HTML print fallback from your code and change it to generate PDF if necessary.
Until OTRS 5, translatable strings could not be extracted from Perl code and Database XML definitions. This
is now possible and makes dummy templates like AAA*.tt obsolete. Please see this section for details.
This section lists changes that you need to examine when porting your package from OTRS 3.3 to 4.
Up to OTRS 4, objects used to be created both centrally and also locally and then handed down to all objects
by passing them to the constructors. With OTRS 4 and later versions, there is now an
ObjectManager that centralizes singleton object creation and access.
This will require you first of all to change all top level Perl scripts (.pl files only!) to load and
provide the ObjectManager to all OTRS objects. Let's look at
otrs.CheckDB.pl from OTRS 3.3 as an example:
use strict;
use warnings;
use File::Basename;
use FindBin qw($RealBin);
use lib dirname($RealBin);
use lib dirname($RealBin) . '/Kernel/cpan-lib';
use lib dirname($RealBin) . '/Custom';
use Kernel::Config;
use Kernel::System::Encode;
use Kernel::System::Log;
use Kernel::System::Main;
use Kernel::System::DB;
# create common objects
my %CommonObject = ();
$CommonObject{ConfigObject} = Kernel::Config->new();
$CommonObject{EncodeObject} = Kernel::System::Encode->new(%CommonObject);
$CommonObject{LogObject} = Kernel::System::Log->new(
LogPrefix => 'OTRS-otrs.CheckDB.pl',
ConfigObject => $CommonObject{ConfigObject},
);
$CommonObject{MainObject} = Kernel::System::Main->new(%CommonObject);
$CommonObject{DBObject} = Kernel::System::DB->new(%CommonObject);
We can see that a lot of code is used to load the packages and create the common objects that must be passed to OTRS objects to be used in the script. With OTRS 4, this looks quite different:
use strict;
use warnings;
use File::Basename;
use FindBin qw($RealBin);
use lib dirname($RealBin);
use lib dirname($RealBin) . '/Kernel/cpan-lib';
use lib dirname($RealBin) . '/Custom';
use Kernel::System::ObjectManager;
# create common objects
local $Kernel::OM = Kernel::System::ObjectManager->new(
'Kernel::System::Log' => {
LogPrefix => 'OTRS-otrs.CheckDB.pl',
},
);
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
The new code is a bit shorter than the old. It is no longer necessary to load all the packages, just the
ObjectManager. Subsequently $Kernel::OM->Get('My::Perl::Package') can
be used to get instances of objects which only have to be created once. The LogPrefix
setting controls the log messages that Kernel::System::Log writes, it could also be
omitted.
From this example you can also deduce the general porting guide when it comes to accessing objects: don't
store them in $Self any more (unless needed for specific reasons). Just fetch and use the
objects on demand like $Kernel::OM->Get('Kernel::System::Log')->Log(...). This also has the
benefit that the Log object will only be created if something must be logged. Sometimes
it could also be useful to create local variables if an object is used many times in a function, like
$DBObject in the example above.
There's not much more to know when porting packages that should be loadable by the
ObjectManager. They should declare the modules they use (via
$Kernel::OM->Get()) like this:
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Log',
'Kernel::System::Main',
);
The @ObjectDependencies declaration is needed for the ObjectManager to
keep the correct order when destroying the objects.
Let's look at Valid.pm from OTRS 3.3 and 4 to see the difference. Old:
package Kernel::System::Valid;
use strict;
use warnings;
use Kernel::System::CacheInternal;
...
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 EncodeObject MainObject)) {
$Self->{$Object} = $Param{$Object} || die "Got no $Object!";
}
$Self->{CacheInternalObject} = Kernel::System::CacheInternal->new(
%{$Self},
Type => 'Valid',
TTL => 60 * 60 * 24 * 20,
);
return $Self;
}
...
sub ValidList {
my ( $Self, %Param ) = @_;
# read cache
my $CacheKey = 'ValidList';
my $Cache = $Self->{CacheInternalObject}->Get( Key => $CacheKey );
return %{$Cache} if $Cache;
# get list from database
return if !$Self->{DBObject}->Prepare( SQL => 'SELECT id, name FROM valid' );
# fetch the result
my %Data;
while ( my @Row = $Self->{DBObject}->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# set cache
$Self->{CacheInternalObject}->Set( Key => $CacheKey, Value => \%Data );
return %Data;
}
New:
package Kernel::System::Valid;
use strict;
use warnings;
our @ObjectDependencies = (
'Kernel::System::Cache',
'Kernel::System::DB',
'Kernel::System::Log',
);
...
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
$Self->{CacheType} = 'Valid';
$Self->{CacheTTL} = 60 * 60 * 24 * 20;
return $Self;
}
...
sub ValidList {
my ( $Self, %Param ) = @_;
# read cache
my $CacheKey = 'ValidList';
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return %{$Cache} if $Cache;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# get list from database
return if !$DBObject->Prepare( SQL => 'SELECT id, name FROM valid' );
# fetch the result
my %Data;
while ( my @Row = $DBObject->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => \%Data
);
return %Data;
}
You can see that the dependencies are declared and the objects are only fetched on demand. We'll talk about
the CacheInternalObject in the next section.
Since Kernel::System::Cache is now also able to cache in-memory,
Kernel::System::CacheInternal was dropped. Please see the previous example for how to
migrate your code: you need to use the global Cache object and pass the
Type settings with every call to Get(), Set(),
Delete() and CleanUp(). The TTL parameter is now optional and
defaults to 20 days, so you only have to specify it in Get() if you require a different
TTL value.
It is especially important to add the Type to CleanUp() as otherwise not
just the current cache type but the entire cache would be deleted.
The backend files of the scheduler moved from Kernel/Scheduler to
Kernel/System/Scheduler. If you have any custom Task Handler modules, you need to move
them also.
Code tags in SOPM files have to be updated. Please do not use $Self any more. In the past
this was used to get access to OTRS objects like the MainObject. Please use the
ObjectManager now. Here is an example for the old style:
<CodeInstall Type="post">
# define function name
my $FunctionName = 'CodeInstall';
# create the package name
my $CodeModule = 'var::packagesetup::' . $Param{Structure}->{Name}->{Content};
# load the module
if ( $Self->{MainObject}->Require($CodeModule) ) {
# create new instance
my $CodeObject = $CodeModule->new( %{$Self} );
if ($CodeObject) {
# start method
if ( !$CodeObject->$FunctionName(%{$Self}) ) {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Could not call method $FunctionName() on $CodeModule.pm."
);
}
}
# error handling
else {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Could not call method new() on $CodeModule.pm."
);
}
}
</CodeInstall>
Now this should be replaced by:
<CodeInstall Type="post"><![CDATA[
$Kernel::OM->Get('var::packagesetup::MyPackage')->CodeInstall();
]]></CodeInstall>
With OTRS 4, the DTL template engine was replaced by Template::Toolkit. Please refer to the Templating section for details on how the new template syntax looks like.
These are the changes that you need to apply when converting existing DTL templates to the new Template::Toolkit syntax:
Table 4.1. Template Changes from OTRS 3.3 to 4
| DTL Tag | Template::Toolkit tag |
$Data{"Name"} |
[% Data.Name %] |
$Data{"Complex-Name"} |
[% Data.item("Complex-Name") %] |
$QData{"Name"} |
[% Data.Name | html %] |
$QData{"Name", "$Length"} |
[% Data.Name | truncate($Length) | html %] |
$LQData{"Name"} |
[% Data.Name | uri %] |
$Quote{"Text", "$Length"} |
cannot be replaced directly, see examples below |
$Quote{"$Config{"Name"}"} |
[% Config("Name") | html %] |
$Quote{"$Data{"Name"}", "$Length"} |
[% Data.Name | truncate($Length) | html %] |
$Quote{"$Data{"Content"}","$QData{"MaxLength"}"} |
[% Data.Name | truncate(Data.MaxLength) | html %] |
$Quote{"$Text{"$Data{"Content"}"}","$QData{"MaxLength"}"} |
[% Data.Content | Translate | truncate(Data.MaxLength) | html %] |
$Config{"Name"} |
[% Config("Name") %] |
$Env{"Name"} |
[% Env("Name") %] |
$QEnv{"Name"} |
[% Env("Name") | html %] |
$Text{"Text with %s placeholders", "String"} |
[% Translate("Text with %s placeholders", "String") | html %] |
$Text{"Text with dynamic %s placeholders", "$QData{Name}"} |
[% Translate("Text with dynamic %s placeholders", Data.Name) | html %] |
'$JSText{"Text with dynamic %s placeholders", "$QData{Name}"}' |
[% Translate("Text with dynamic %s placeholders", Data.Name) | JSON %] |
"$JSText{"Text with dynamic %s placeholders", "$QData{Name}"}" |
[% Translate("Text with dynamic %s placeholders", Data.Name) | JSON %] |
$TimeLong{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("TimeLong") %] |
$TimeShort{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("TimeShort") %] |
$Date{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("Date") %] |
<-- dtl:block:Name -->...<-- dtl:block:Name --> |
[% RenderBlockStart("Name") %]...[% RenderBlockEnd("Name") %] |
<-- dtl:js_on_document_complete -->...<-- dtl:js_on_document_complete --> |
[% WRAPPER JSOnDocumentComplete %]...[% END %] |
<-- dtl:js_on_document_complete_placeholder --> |
[% PROCESS JSOnDocumentCompleteInsert %] |
$Include{"Copyright"} |
[% InsertTemplate("Copyright") %] |
There is also a helper script bin/otrs.MigrateDTLtoTT.pl that will automatically port
the DTL files to Template::Toolkit syntax for you. It might fail if you have errors in your DTL, please
correct these first and re-run the script afterwards.
There are a few more things to note when porting your code to the new template engine:
All language files must now have the use utf8; pragma.
Layout::Get() is now deprecated. Please use Layout::Translate()
instead.
All occurrences of $Text{""} in Perl code must now be replaced by calls to
Layout::Translate().
This is because in DTL there was no separation between template and data. If DTL-Tags were inserted as part of some data, the engine would still parse them. This is no longer the case in Template::Toolkit, there is a strict separation of template and data.
Hint: should you ever need to interpolate tags in data, you can use the
Interpolate filter for this ([% Data.Name | Interpolate
%]). This is not recommended for security and performance reasons!
For the same reason, dynamically injected JavaScript that was enclosed by
dtl:js_on_document_complete will not work any more. Please use
Layout::AddJSOnDocumentComplete() instead of injecting this as template data.
You can find an example for this in
Kernel/System/DynamicField/Driver/BaseSelect.pm.
Please be careful with pre output filters (the ones configured in
Frontend::Output::FilterElementPre). They still work, but they will prevent
the template from being cached. This could lead to serious performance issues. You should
definitely not have any pre output filters that operate on all templates, but
limit them to certain templates via configuration setting.
The post output filters
(Frontend::Output::FilterElementPost) don't have such strong negative
performance effects. However, they should also be used carefully, and not for all templates.
With OTRS 4, we've also updated FontAwesome to a new version. As a consequence, the icons CSS classes have
changed. While previously icons were defined by using a schema like icon-{iconname}, it
is now fa fa-{iconname}.
Due to this change, you need to make sure to update all custom frontend module registrations which make use
of icons (e.g. for the top navigation bar) to use the new schema. This is also true for templates where
you're using icon elements like <i class="icon-{iconname}"></i>.
With OTRS 4, in Unit Tests $Self no longer provides common objects like the
MainObject, for example. Please always use $Kernel::OM->Get('...') to fetch
these objects.
If you use any custom ticket history types, you have to take two steps for them to be displayed correctly
in AgentTicketHistory of OTRS 4+.
Firstly, you have to register your custom ticket history types via SysConfig. This could look like:
<ConfigItem Name="Ticket::Frontend::HistoryTypes###100-MyCustomModule" Required="1" Valid="1">
<Description Translatable="1">Controls how to display the ticket history entries as readable values.</Description>
<Group>Ticket</Group>
<SubGroup>Frontend::Agent::Ticket::ViewHistory</SubGroup>
<Setting>
<Hash>
<Item Key="MyCustomType" Translatable="1">Added information (%s)</Item>
</Hash>
</Setting>
</ConfigItem>
The second step is to translate the English text that you provided for the custom ticket history type in your translation files, if needed. That's it!
If you are interested in the details, please refer to this commit for additional information about the changes that happened in OTRS.