Package Porting

From OTRS 5 to 6
Date and time calculation
Adding the drag & drop multiupload
Improvements to administration screens
Add breadcrumbs to administration screens
Add "save" and "save and finish" buttons to administration screens
Migrate configuration files
XML configuration file format
Perl configuration file format
Perldoc structure changed
Improvements to templating and working with JavaScript
JavaScript removed from templates
Template files for rich text editor removed
Translations in JavaScript files
Handover data from Perl to JavaScript
HTML templates for JavaScript
Checking user permissions
Ticket API changes
TicketGet()
LinkObject Events
Article API changes
Meta Article API
Article Backend API
Article Search Index
SysConfig API changes
LinkObject API changes
Communication Log support for additional PostMaster Filters
Process MailQueue for unit tests
Widget Handling in Ticket Zoom Screen
From OTRS 4 to 5
Kernel/Output/HTML restructured
Pre-Output-Filters
IE 8 and IE 9
GenericInterface API change in "Ticket" connector
Preview functions in dynamic statistics
HTML print discarded
Translation string extraction improved
From OTRS 3.3 to 4
New Object Handling
CacheInternalObject removed
Scheduler backend files moved
Update code sections in SOPM files
New Template Engine
New FontAwesome version
Unit Tests
Custom Ticket History types

With 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.

From OTRS 5 to 6

This section lists changes that you need to examine when porting your package from OTRS 5 to 6.

Date and time calculation

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.

Adding the drag & drop multiupload

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.

Note

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 multiupload 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.

Improvements to administration screens

Add breadcrumbs to administration screens

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 %]
                
Add "save" and "save and finish" buttons to administration screens

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>
                

Migrate configuration files

XML configuration file format

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.
                

Perl configuration file format

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 neccessary 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.

Perldoc structure changed

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.

Improvements to templating and working with JavaScript

JavaScript removed from templates

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 occurences, 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.

Template files for rich text editor removed

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 with CustomerRichTextEditor.tt and apply following code instead:

$LayoutObject->CustomerSetRichTextParameters(
    Data => \%Param,
);
                

Translations in JavaScript files

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.

Handover data from Perl to JavaScript

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).

HTML templates for JavaScript

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.

Checking user permissions

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',
);
            

Ticket API changes

TicketGet()

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.

LinkObject Events

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 (i.e. Ticket).

SourceKey

Key of the link source object (i.e. TicketID).

TargetObject

Name of the link target object (i.e. FAQItem).

TargetKey

Key of the link target object (i.e. FAQItemID).

Type

Type of the link (i.e. 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>
                

Article API changes

In OTRS 6, changes to Article API have been made, in preparations for new Omni Channel infrastructure.

Meta Article API

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' );
                

Article Backend API

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)

Note

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.

Note

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;
}
                

Article Search Index

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 parameterNew parameter
FromMIMEBase_From
ToMIMEBase_To
CcMIMEBase_Cc
SubjectMIMEBase_Subject
BodyMIMEBase_Body
AttachmentNameMIMEBase_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, i.e. 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.

SysConfig API changes

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 previouly mentioned steps and it can be used for multiple settings at once.

Note

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)
        },
        ...
    ],
);
            

LinkObject API changes

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,
);
            

Communication Log support for additional PostMaster Filters

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.

Process MailQueue for unit tests

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.

Widget Handling in Ticket Zoom Screen

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 AgentTicketZoom 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>
            

Note

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.

From OTRS 4 to 5

This section lists changes that you need to examine when porting your package from OTRS 4 to 5.

Kernel/Output/HTML restructured

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.

Pre-Output-Filters

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.

IE 8 and IE 9

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.

GenericInterface API change in "Ticket" connector

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',
        },
    ],
},
]
            

Preview functions in dynamic statistics

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.

HTML print discarded

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.

Translation string extraction improved

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.

From OTRS 3.3 to 4

This section lists changes that you need to examine when porting your package from OTRS 3.3 to 4.

New Object Handling

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.

CacheInternalObject removed

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.

Warning

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.

Scheduler backend files moved

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.

Update code sections in SOPM files

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>
            

New Template Engine

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.

New FontAwesome version

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>.

Unit Tests

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.

Custom Ticket History types

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.