Chapter 2. Hacking OTRS

Table of Contents

How it Works
Config Mechanism
Default Config
Custom Config
Accessing Config Options
XML Config Options
Database Mechanism
How it works
Database Drivers
Supported Databases
Log Mechanism
Use and Syntax
Example
Skins
Skin Basics
How skins are loaded
Creating a New Skin
The CSS and JavaScript "Loader"
How it works
Basic Operation
Configuring the Loader: JavaScript
Configuring the Loader: CSS
Templating Mechanism
Template Commands
Using a template file
How to Extend it
Module Format
Core Modules
Frontend Modules
Old Module Descriptions
Object Basics
Object Options
Search Options
Config Naming
Config File
NavBar Settings
Screen flow
Writing an OTRS module for a new object
What we want to write
Default Config File
Frontend Module
Core Module
dtl Template File
Language File
Summary
How to Publish your OTRS Extensions
Package Management
Package Distribution
Package Commands
Package Building
Package Spec File
Example .sopm
Package Build
Package Life Cycle - Install/Upgrade/Uninstall
How to Upgrade your OTRS Extensions to Newer Versions of OTRS
Upgrading OTRS Extensions from 2.4 to 3.0
Translation of configuration file descriptions.
Configuration of NavBarModule was Changed
Configuration of NavBar was Changed
Configuration Setting Frontend::NavBarModule was Renamed
PreferencesGroups configuration

Abstract

In this chapter, we'll take a closer look at how OTRS works, how to extend it and how to publish your OTRS extensions.

How it Works

Config Mechanism

Default Config

There are different default config files. The main one, which comes with the framework, is:

Kernel/Config/Defaults.pm

This file should be left untouched as it is automatically updated on framework updates. There is also a sub directory where you can store the default config files for your own modules. These files are used automatically.

The directory is located under:

$OTRS_HOME/Kernel/Config/Files/*.pm

And could look as follows:

Kernel/config/Files/Calendar.pm

# module reg and nav bar
$Self->{'Frontend::Module'}->{'AgentCalendar'} = {
    Description => 'Calendar',
    NavBarName => 'Ticket',
    NavBar => [
        {
            Description => 'Calendar',
            Name => 'Calendar',
            Image => 'calendar.png',
            Link => 'Action=AgentCalendar',
            NavBar => 'Ticket',
            Prio => 5000,
            AccessKey => 'c',
        },
    ],
};

# show online customers
$Self->{'Frontend::NotifyModule'}->{'80-ShowCalendarEvents'} = {
    Module => 'Kernel::Output::HTML::NotificationCalendar',
};
        

Custom Config

If you want to change a config option, copy it to

Kernel/Config.pm

and set the new option. This file will be read out last and so all default config options are overwritten with your settings.

This way it is easy to handle updates - you just need the Kernel/Config.pm.

Accessing Config Options

You can read and write (for one request) the config options via the core module "Kernel::Config". The config object is a base object and thus available in each Frontend Module.

If you want to access a config option:

my $ConfigOption = $Self->{ConfigObject}->Get('Prefix::Option');
        

If you want to change a config option at runtime and just for this one request/process:

$Self->{ConfigObject}->Set(
    Key => 'Prefix::Option'
    Value => 'SomeNewValue',
);
        

XML Config Options

XML config files are located under:

$OTRS_HOME/Kernel/Config/Files/*.xml

Each config file has the following layout:

<?xml version="1.0" encoding="utf-8" ?>
<otrs_config version="1.0" init="Changes">

    <!--  config items will be here -->

</otrs_config>
        

The "init" attribute describes where the config options should be loaded. There are different levels available and will be loaded/overloaded in the following order: "Framework" (for framework settings e. g. session option), "Application" (for application settings e. g. ticket options), "Config" (for extensions to existing applications e. g. ITSM options) and "Changes" (for custom development e. g. to overwrite framework or ticket options).

If you want to add config options, here is an example:

<ConfigItem Name="Ticket::Hook" Required="1" Valid="1" ConfigLevel="300">
    <Description Lang="en">The identifyer for a ticket. The default is Ticket#.</Description>
    <Description Lang="de">Ticket-Identifikator. Als Standard wird Ticket# verwendet.</Description>
    <Group>Ticket</Group>
    <SubGroup>Core::Ticket</SubGroup>
    <Setting>
        <String Regex="">Ticket#</String>
    </Setting>
</ConfigItem>
        

If "required" is set to "1", the config variable is included and cannot be disabled.

If "valid" is set to "1", the config variable is active. If it is set to "0", the config variable is inactive.

If the optional attribute "ConfigLevel" is set, the config variable might not be edited by the administrator, depending on his own config level. The config variable "ConfigLevel" sets the level of technical experience of the administrator. It can be 100 (Expert), 200 (Advanced) or 300 (Beginner). As a guideline which config level should be given to an option, it is recommended that all options having to do with the configuration of externel interaction, like Sendmail, LDAP, SOAP, and others should get a config level of at least 200 (Advanced).

The config variable is defined in the "setting" element.

Types of XML Config Variables

The XML config settings support various types of variables.

String

A config element for numbers and single-line strings. Checking the validity with a regex is possible. The check attribute checks elements on the file system. This contains files and directories.

<Setting>
    <String Regex="" Check="File"></String>
</Setting>
            

Textarea

A config element for multiline text.

<Setting>
    <TextArea Regex=""></TextArea>
</Setting>
            

Options

This config element offers preset values as a pull-down menu.

<Setting>
    <Option SelectedID="Key">
        <Item Key=""></Item>
        <Item Key=""></Item>
    </Option>
</Setting>
            

Array

With this config element arrays can be displayed.

<Setting>
    <Array>
        <Item></Item>
        <Item></Item>
    </Array>
</Setting>
            

Hash

With this config element hashes can be displayed.

<Setting>
    <Hash>
        <Item Key=""></Item>
        <Item Key=""></Item>
    </Hash>
</Setting>
            

Hash with SubArray, SubHash

A hash can contain content, arrays or hashes.

<Setting>
    <Hash>
        <Item Key=""></Item>
        <Item Key="">
            <Hash>
                <Item Key=""></Item>
                <Item Key=""></Item>
            </Hash>
        </Item>
        <Item Key="">
            <Array>
                <Item></Item>
                <Item></Item>
            </Array>
        </Item>
        <Item Key=""></Item>
    </Hash>
</Setting>
            

FrontendModuleReg (NavBar)

Module registration for Agent Interface.

<Setting>
    <FrontendModuleReg>
        <Group>group1</Group>
        <Group>group2</Group>
        <Description>Logout</Description>
        <Title></Title>
        <NavBarName></NavBarName>
        <NavBar>
            <Description>Logout</Description>
            <Name>Logout</Name>
            <Image>exit.png</Image>
            <Link>Action=Logout</Link>
            <NavBar></NavBar>
            <Type></Type>
            <Block>ItemPre</Block>
            <AccessKey>l</AccessKey>
            <Prio>100</Prio>
        </NavBar>
    </FrontendModuleReg>
</Setting>
            

FrontendModuleReg (NavBarModule)

Module registration for Admin Interface

<Setting>
    <FrontendModuleReg>
        <Group>admin</Group>
        <Group>admin2</Group>
        <Description>Admin</Description>
        <Title>User</Title>
        <NavBarName>Admin</NavBarName>
        <NavBarModule>
            <Module>Kernel::Output::HTML::NavBarModuleAdmin</Module>
            <Name>Users</Name>
            <Block>Block1</Block>
            <Prio>100</Prio>
        </NavBarModule>
    </FrontendModuleReg>
</Setting>
            

Database Mechanism

OTRS comes with a database layer that supports different databases.

How it works

The database layer (Kernel::System::DB) has two input options: SQL and XML.

SQL

The SQL interface should be used for normal database actions (SELECT, INSERT, UPDATE, ...). It can be used like a normal Perl DBI interface.

INSERT/UPDATE/DELETE
$Self->{DBObject}->Do(
    SQL=> "INSERT INTO table (name, id) VALUES ('SomeName', 123)",
);

$Self->{DBObject}->Do(
    SQL=> "UPDATE table SET name = 'SomeName', id = 123",
);

$Self->{DBObject}->Do(
    SQL=> "DELETE FROM table WHERE id = 123",
);
            
SELECT
my $SQL = "SELECT id FROM table WHERE tn = '123'";

$Self->{DBObject}->Prepare(SQL => $SQL, Limit => 15);

while (my @Row = $Self->{DBObject}->FetchrowArray()) {
    $Id = $Row[0];
}
return $Id;
            

Note

Take care to use Limit as param and not in the SQL string because not all databases support LIMIT in SQL strings.

my $SQL = "SELECT id FROM table WHERE tn = ? AND group = ?";

$Self->{DBObject}->Prepare(
    SQL   => $SQL,
    Limit => 15,
    Bind  => [ $Tn, $Group ],
);

while (my @Row = $Self->{DBObject}->FetchrowArray()) {
    $Id = $Row[0];
}
return $Id;
            

Note

Use the Bind attribute where ever you can, especially for long statements. If you use Bind you do not need the function Quote().

QUOTE

String:

my $QuotedString = $Self->{DBObject}->Quote("It's a problem!");
                    

Integer:

my $QuotedInteger = $Self->{DBObject}->Quote('123', 'Integer');
                    

Number:

my $QuotedNumber = $Self->{DBObject}->Quote('21.35', 'Number');
                    

Note

Please use the Bind attribute instead of Quote() where ever you can.

XML

The XML interface should be used for INSERT, CREATE TABLE, DROP TABLE and ALTER TABLE. As this syntax is different from database to database, using it makes sure that you write applications that can be used in all of them.

Note

The <Insert> has changed in >=2.2. Values are now used in content area (not longer in attribut Value).

INSERT
<Insert Table="some_table">
    <Data Key="id">1</Data>
    <Data Key="description" Type="Quote">exploit</Data>
</Insert>
            
CREATE TABLE

Possible data types are: BIGINT, SMALLINT, INTEGER, VARCHAR (Size=1-1000000), DATE (Format: yyyy-mm-dd hh:mm:ss) and LONGBLOB.

<TableCreate Name="calendar_event">
    <Column Name="id" Required="true" PrimaryKey="true" AutoIncrement="true" Type="BIGINT"/>
    <Column Name="title" Required="true" Size="250" Type="VARCHAR"/>
    <Column Name="content" Required="false" Size="250" Type="VARCHAR"/>
    <Column Name="start_time" Required="true" Type="DATE"/>
    <Column Name="end_time" Required="true" Type="DATE"/>
    <Column Name="owner_id" Required="true" Type="INTEGER"/>
    <Column Name="event_status" Required="true" Size="50" Type="VARCHAR"/>
    <Index Name="calendar_event_title">
        <IndexColumn Name="title"/>
    </Index>
    <Unique Name="calendar_event_title">
        <UniqueColumn Name="title"/>
    </Unique>
    <ForeignKey ForeignTable="users">
        <Reference Local="owner_id" Foreign="id"/>
    </ForeignKey>
</TableCreate>
            
DROP TABLE
<TableDrop Name="calendar_event"/>
            
ALTER TABLE

The following shows an example of add, change and drop columns.

<TableAlter Name="calendar_event">
    <ColumnAdd Name="test_name" Type="varchar" Size="20" Required="1"/>

    <ColumnChange NameOld="test_name" NameNew="test_title" Type="varchar" Size="30" Required="1"/>

    <ColumnChange NameOld="test_title" NameNew="test_title" Type="varchar" Size="100" Required="0"/>

    <ColumnDrop Name="test_title"/>

    <IndexCreate Name="index_test3">
        <IndexColumn Name="test3"/>
    </IndexCreate>

    <IndexDrop Name="index_test3"/>

    <UniqueCreate Name="uniq_test3">
        <UniqueColumn Name="test3"/>
    </UniqueCreate>

    <UniqueDrop Name="uniq_test3"/>
</TableAlter>
            

The next shows an example how to rename a table.

<TableAlter NameOld="calendar_event" NameNew="calendar_event_new"/>
            
Code to process XML
my @XMLARRAY = @{$Self->ParseXML(String => $XML)};

my @SQL = $Self->{DBObject}->SQLProcessor(
    Database => \@XMLARRAY,
);
push(@SQL, $Self->{DBObject}->SQLProcessorPost());

for (@SQL) {
    $Self->{DBObject}->Do(SQL => $_);
}
            

Database Drivers

The database drivers are located under $OTRS_HOME/Kernel/System/DB/*.pm.

Supported Databases

  • MySQL

  • PostgreSQL

  • Oracle

  • MSSQL

  • DB2

Log Mechanism

OTRS comes with a log backend that can be used for application logging and debugging.

Use and Syntax

All module layers have ready-made Log Objects which can be used by

$Self->{LogObject}->Log(
    Priority => 'error',
    Message => 'Need something!',
);
        

Example

The following example shows how to use the log mechanism without a module layer.

use Kernel::Config;
use Kernel::System::Encode;
use Kernel::System::Log;

my $ConfigObject = Kernel::Config->new();
my $EncodeObject = Kernel::System::Encode->new(
    ConfigObject => $ConfigObject,
);
my $LogObject    = Kernel::System::Log->new(
    ConfigObject => $ConfigObject,
);

$Self->{LogObject}->Log(
    Priority => 'error',
    Message => 'Need something!',
);
        

Skins

Since OTRS 3.0, the visual appearance of OTRS is controlled by "skins".

A skin is a set of CSS and image files, which together control how the GUI is presented to the user. Skins do not change the HTML content that is generated by OTRS (this is what "Themes" do), but they control how it is displayed. With the help of modern CSS standards it is possible to change the display thoroughly (e.g. repositioning parts of dialogs, hiding elements, ...).

Skin Basics

All skins are in $OTRS_HOME/var/httpd/htdocs/skins/$SKIN_TYPE/$SKIN_NAME. There are two types of skins: agent and customer skins.

Each of the agents can select individually, which of the installed agent skins they want to "wear".

For the customer interface, a skin has to be selected globally with the config setting Loader::Customer::SelectedSkin. All customers will see this skin.

How skins are loaded

It is important to note that the "default" skin will always be loaded first. If the agent selected another skin than the "default" one, then the other one will be loaded only after the default skin. By "loading" here we mean that OTRS will put tags into the HTML content which cause the CSS files to be loaded by the browser. Let's see an example of this:

    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css-cache/CommonCSS_179376764084443c181048401ae0e2ad.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/ivory/css-cache/CommonCSS_e0783e0c2445ad9cc59c35d6e4629684.css" />
            

Here it can clearly be seen that the default skin is loaded first, and then the custom skin specified by the agent. In this example, we see the result of the activated loader (Loader::Enabled set to 1), which gathers all CSS files, concatenates and minifies them and serves them as one chunk to the browser. This saves bandwith and also reduces the number of HTTP requests. Let's see the same example with the Loader turned off:

    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Reset.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Default.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Header.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.OverviewControl.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.OverviewSmall.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.OverviewMedium.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.OverviewLarge.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Footer.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Grid.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Form.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Table.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Widget.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.WidgetMenu.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.TicketDetail.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Tooltip.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Dialog.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Print.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Agent.CustomerUser.GoogleMaps.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/default/css/Core.Agent.CustomerUser.OpenTicket.css" />
    <link rel="stylesheet" href="/otrs-web/skins/Agent/ivory/css/Core.Dialog.css" />
            

Here we can better see the individual files that come from the skins.

There are different types of CSS files: common files which must always be loaded, and "module-specific" files which are only loaded for special modules within the OTRS framework.

In addition, it is possible to specify CSS files which only must be loaded on IE7 or IE8 (in the case of the customer interface, also IE6). This is unfortunate, but it was not possible to develop a modern GUI on these browsers without having special CSS for them.

For details regarding the CSS file types, please see the section on the Loader.

For each HTML page generation, the loader will first take all configured CSS files from the default skin, and then for each file look if it is also available in a custom skin (if a custom skin is selected) and load them after the default files.

That means a) that CSS files in custom skins need to have the same names as in the default skins, and b) that a custom skin does not need to have all files of the default skin. That is the big advantage of loading the default skin first: a custom skin has all default CSS rules present and only needs to change those which should result in a different display. That can often be done in a single file, like in the example above.

Another effect of this is that you need to be careful to overwrite all default CSS rules in your custom skins that you want to change. Let's see an example:

.Header h1 {
    font-weight: bold;
    color: #000;
}
            

This defines special headings inside of the .Header element as bold, black text. Now if you want to change that in your skin to another color and normal text, it is not enough to write

.Header h1 {
    color: #F00;
}
            

Because the original rule for font-weight still applies. You need to override it explicitly:

.Header h1 {
    font-weight: normal;
    color: #F00;
}
            

Creating a New Skin

In this section, we will be creating a new agent skin which replaces the default OTRS background color (white) with a custom company color (light grey) and the default logo by a custome one. Also we will configure that skin to be the one which all agents will see by default.

There are only two simple steps we need to take to achieve this goal:

  • create the skin files

  • configure the new logo and

  • make the skin known to the OTRS system.

Let's start by creating the files needed for our new skin. First of all, we need to create a new folder for this skin (we'll call it "custom"). This folder will be $OTRS_HOME/var/httpd/htdocs/skins/Agent/custom.

In there, we need to place the new CSS file in a new directory css which defines the new skin's appearance. We'll call it Core.Default.css (remember that it must have the same name as one of the files in the "default" skin). This is the code needed for the CSS file:

body {
    background-color: #c0c0c0; /* not very beautiful but it meets our purpose */
}
            

Now follows the second step, adding a new logo and making the new skin known to the OTRS system. For this, we first need to place our custom logo (e.g. logo.png) in a new directory (e.g. img) in our skin directory. Then we need to create a new config file $OTRS_HOME/Kernel/Config/Files/CustomSkin.xml, which will contain the needed settings as follows:

<?xml version="1.0" encoding="utf-8" ?>
<otrs_config version="1.0" init="Framework">
    <ConfigItem Name="AgentLogo" Required="0" Valid="1">
        <Description Translatable="1">The logo shown in the header of the agent interface. The URL to the image must be a relative URL to the skin image directory.</Description>
        <Group>Framework</Group>
        <SubGroup>Frontend::Agent</SubGroup>
        <Setting>
            <Hash>
                <Item Key="URL">skins/Agent/custom/img/logo.png</Item>
                <Item Key="StyleTop">13px</Item>
                <Item Key="StyleRight">75px</Item>
                <Item Key="StyleHeight">67px</Item>
                <Item Key="StyleWidth">244px</Item>
            </Hash>
        </Setting>
    </ConfigItem>
    <ConfigItem Name="Loader::Agent::Skin###100-custom" Required="0" Valid="1">
        <Description Translatable="1">Custom skin for the development manual.</Description>
        <Group>Framework</Group>
        <SubGroup>Frontend::Agent</SubGroup>
        <Setting>
            <Hash>
                <Item Key="InternalName">custom</Item>
                <Item Key="VisibleName">Custom</Item>
                <Item Key="Description">Custom skin for the development manual.</Item>
                <Item Key="HomePage">www.yourcompany.com</Item>
            </Hash>
        </Setting>
    </ConfigItem>
</otrs_config>
            

To make this configuration active, we need to navigate to the SysConfig module in the admin area of OTRS (alternatively, you can run the script $OTRS_HOME/bin/otrs.RebuildConfig.pl). This will regenerate the Perl cache of the XML configuration files, so that our new skin is now known and can be selected in the system. To make it the default skin that new agents see before they made their own skin selection, edit the SysConfig setting "Loader::Agent::DefaultSelectedSkin" and set it to "custom".

In conclusion: to create a new skin in OTRS, we had to place the new logo file, and create one CSS and one XML file, resulting in three new files:

$OTRS_HOME/Kernel/Config/Files/CustomSkin.xml
$OTRS_HOME/var/httpd/htdocs/skins/Agent/custom/img/custom-logo.png
$OTRS_HOME/var/httpd/htdocs/skins/Agent/custom/css/Core.Header.css
            

The CSS and JavaScript "Loader"

Starting with OTRS 3.0, the CSS and JavaScript code in OTRS grew to a large amount. To be able to satisfy both development concerns (good maintainability by a large number of separate files) and performance issues (making few HTTP requests and serving minified content without unneccessary whitespace and documentation) had to be addressed. To achieve these goals, the Loader was invented.

How it works

To put it simple, the Loader

  • determines for each request precisely which CSS and JavaScript files are needed at the client side by the current application module

  • collects all the relevant data

  • minifies the data, removing unneccessary whitespace and documentation

  • serves it to the client in only a few HTTP reqests instead of many individual ones, allowing the client to cache these snippets in the browser cache

  • perform these tasks in a highly performing way, utilizing the caching mechanisms of OTRS.

Of course, there is a little bit more detailed involved, but this should suffice as a first overview.

Basic Operation

With the configuration settings Loader::Enabled::CSS and Loader::Enabled::JavaScript, the loader can be turned on and off for JavaScript and CSS, respectively (it is on by default).

Warning

Because of rendering problems in Internet Explorer, the Loader cannot be turned off for CSS files for this client browser (config setting will be overridden). Up to version 8, Internet Explorer cannot handle more than 32 CSS files on a page.

To learn about how the Loader works, please turn it off in your OTRS installation with the aforementioned configuration settings. Now look at the source code of the application module that you are currently using in this OTRS system (after a reload, of course). You will see that there are many CSS files loaded in the <head> section of the page, and many JavaScript files at the bottom of the page, just before the closing </body> element.

Having the content like this in many individual files with a readable formatting makes the development much easier, and even possible at all. However, this has the disadvantage of a large number of HTTP requests (network latency has a big effect) and unneccessary content (whitespace and documentation) which needs to be transferred to the client.

The Loader solves this problem by performing the steps outlined in the short description above. Please turn on the Loader again and reload your page now. Now you can see that there are only very few CSS and JavaScript tags in the HTML code, like this:

<script type="text/javascript" src="/otrs30-dev-web/js/js-cache/CommonJS_d16010491cbd4faaaeb740136a8ecbfd.js"></script>

<script type="text/javascript" src="/otrs30-dev-web/js/js-cache/ModuleJS_b54ba9c085577ac48745f6849978907c.js"></script>
            

What just happened? During the original request generating the HTML code for this page, the Loader generated these two files (or took them from the cache) and put the shown <script> tags on the page which link to these files, instead of linking to all relevant JavaScript files separately (as you saw it without the loader being active).

The CSS section looks a little more complicated:

    <link rel="stylesheet" type="text/css" href="/otrs30-dev-web/skins/Agent/default/css-cache/CommonCSS_00753c78c9be7a634c70e914486bfbad.css" />

<!--[if IE 7]>
    <link rel="stylesheet" type="text/css" href="/otrs30-dev-web/skins/Agent/default/css-cache/CommonCSS_IE7_59394a0516ce2e7359c255a06835d31f.css" />
<![endif]-->

<!--[if IE 8]>
    <link rel="stylesheet" type="text/css" href="/otrs30-dev-web/skins/Agent/default/css-cache/CommonCSS_IE8_ff58bd010ef0169703062b6001b13ca9.css" />
<![endif]-->
            

The reason is that Internet Explorer 7 and 8 need special treatment in addition to the default CSS because of their lacking support of web standard technologies. So we have some normal CSS that is loaded in all browsers, and some special CSS that is inside of so-called "conditional comments" which cause it to be loaded only by Internet Explorer 7/8. All other browsers will ignore it.

Now we have outlined how the loader works. Let's look at how you can utilize that in your own OTRS extensions by adding configuration data to the loader, telling it to load additional or alternative CSS or JavaScript content.

Configuring the Loader: JavaScript

To be able to operate correctly, the Loader needs to know which content it has to load for a particular OTRS application module. First, it will look for JavaScript files which always have to be loaded, and then it looks for special files which are only relevant for the current application module.

Common JavaScript

The list of JavaScript files to be loaded is configured in the configuration settings Loader::Agent::CommonJS (for the agent interface) and Loader::Customer::CommonJS (for the customer interface).

These settings are designed as hashes, so that OTRS extensions can add their own hash keys for additional content to be loaded. Let's look at an example:

<ConfigItem Name="Loader::Agent::CommonJS###000-Framework" Required="1" Valid="1">
    <Description Translatable="1">List of JS files to always be loaded for the agent interface.</Description>
    <Group>Framework</Group>
    <SubGroup>Core::Web</SubGroup>
    <Setting>
        <Array>
            <Item>thirdparty/json/json2.js</Item>
            <Item>thirdparty/jquery-1.4.2/jquery.js</Item>

            ...

            <Item>Core.App.js</Item>
            <Item>Core.Agent.js</Item>
            <Item>Core.Agent.Search.js</Item>
        </Array>
    </Setting>
</ConfigItem>
                

This is the list of JavaScript files which always need to be loaded for the agent interface of OTRS.

To add new content which is supposed to be loaded always in the agent interface, just add an XML configuration file with another hash entry:

<ConfigItem Name="Loader::Agent::CommonJS###100-CustomPackage" Required="0" Valid="1">
    <Description Translatable="1">List of JS files to always be loaded for the agent interface for package "CustomPackage".</Description>
    <Group>Framework</Group>
    <SubGroup>Core::Web</SubGroup>
    <Setting>
        <Array>
            <Item>CustomPackage.App.js</Item>
        </Array>
    </Setting>
</ConfigItem>
                

Simple, isn't it?

Module-Specific JavaScript

Not all JavaScript is usable for all application modules of OTRS. Therefore it is possible to specify module-specific JavaScript files. Whenever a certain module is used (such as AgentDashboard), the module-specific JavaScript for this module will also be loaded. The configuration is done in the frontend module registration in the XML configurations. Again, an example:

<ConfigItem Name="Frontend::Module###AgentDashboard" Required="0" Valid="1">
    <Description Translatable="1">Frontend module registration for the agent interface.</Description>
    <Group>Framework</Group>
    <SubGroup>Frontend::Agent::ModuleRegistration</SubGroup>
    <Setting>
        <FrontendModuleReg>
            <Description>Agent Dashboard</Description>
            <Title></Title>
            <NavBarName>Dashboard</NavBarName>
            <NavBar>
                <Description Translatable="1"></Description>
                <Name Translatable="1">Dashboard</Name>
                <Link>Action=AgentDashboard</Link>
                <NavBar>Dashboard</NavBar>
                <Type>Menu</Type>
                <Description Translatable="1"></Description>
                <Block>ItemArea</Block>
                <AccessKey>d</AccessKey>
                <Prio>50</Prio>
            </NavBar>
            <Loader>
                <JavaScript>thirdparty/flot/excanvas.js</JavaScript>
                <JavaScript>thirdparty/flot/jquery.flot.js</JavaScript>
                <JavaScript>Core.UI.Chart.js</JavaScript>
                <JavaScript>Core.UI.DnD.js</JavaScript>
                <JavaScript>Core.Agent.Dashboard.js</JavaScript>
            </Loader>
        </FrontendModuleReg>
    </Setting>
</ConfigItem>
                

It is possible to put a <Loader> tag in the frontend module registrations which may contain <JavaScript> tags, one for each file that is supposed to be loaded for this application module.

Now you have all information you need to configure the way the Loader handles JavaScript code.

There is one special case: for ToolbarModules, you can also add custom JavaScript files. Just add a JavaScript attribute to the configuration like this:

<ConfigItem Name="Frontend::ToolBarModule###410-Ticket::AgentTicketEmail" Required="0" Valid="1">
    <Description Translatable="1">Toolbar Item for a shortcut.</Description>
    <Group>Ticket</Group>
    <SubGroup>Frontend::Agent::ToolBarModule</SubGroup>
    <Setting>
        <Hash>
            <Item Key="Module">Kernel::Output::HTML::ToolBarLink</Item>
            <Item Key="Name">New email ticket</Item>
            <Item Key="Priority">1009999</Item>
            <Item Key="Link">Action=AgentTicketEmail</Item>
            <Item Key="Action">AgentTicketEmail</Item>
            <Item Key="AccessKey">l</Item>
            <Item Key="CssClass">EmailTicket</Item>
            <Item Key="JavaScript">OTRS.Agent.CustomToolbarModule.js</Item>
        </Hash>
    </Setting>
</ConfigItem>
                

Configuring the Loader: CSS

The loader handles CSS files very similar to JavaScript files, as described in the previous section, and extending the settings works in the same way too.

Common CSS

The way common CSS is handled is very similar to the way common JavaScript is loaded. Here, the configuration settings are called Loader::Agent::CommonCSS and Loader::Customer::CommonCSS, respectively.

However, as we already noted above, Internet Explorer 7 and 8 (and for the customer interface also 6) need special treatment. That's why there are special configuration settings for them, to specify common CSS which should only be loaded in these browsers. The respective settings are Loader::Agent::CommonCSS::IE7, Loader::Agent::CommonCSS::IE8, Loader::Customer::CommonCSS::IE6, Loader::Customer::CommonCSS::IE7 and Loader::Customer::CommonCSS::IE8.

An example:

<ConfigItem Name="Loader::Agent::CommonCSS::IE8###000-Framework" Required="1" Valid="1">
    <Description Translatable="1">List of IE8-specific CSS files to always be loaded for the agent interface.</Description>
    <Group>Framework</Group>
    <SubGroup>Core::Web</SubGroup>
    <Setting>
        <Array>
            <Item>Core.OverviewSmall.IE8.css</Item>
        </Array>
    </Setting>
</ConfigItem>
                

This is the list of common CSS files for the agent interface which should only be loaded in Internet Explorer 8.

Module-Specific CSS

Module-specific CSS is handled very similar to the way module-specific JavaScript is handled. It is also configured in the frontend module registrations. Example:

<ConfigItem Name="Frontend::Module###Admin" Required="0" Valid="1">
    <Description Translatable="1">Frontend module registration for the agent interface.</Description>
    <Group>Framework</Group>
    <SubGroup>Frontend::Admin::ModuleRegistration</SubGroup>
    <Setting>
        <FrontendModuleReg>
            <Group>admin</Group>
            <Description>Admin-Area</Description>
            <Title></Title>
            <NavBarName>Admin</NavBarName>
            <NavBar>
                <Type>Menu</Type>
                <Description Translatable="1"></Description>
                <Block>ItemArea</Block>
                <Name Translatable="1">Admin</Name>
                <Link>Action=Admin</Link>
                <NavBar>Admin</NavBar>
                <AccessKey>a</AccessKey>
                <Prio>10000</Prio>
            </NavBar>
            <NavBarModule>
                <Module>Kernel::Output::HTML::NavBarModuleAdmin</Module>
            </NavBarModule>
            <Loader>
                <CSS>Core.Agent.Admin.css</CSS>
                <CSS_IE7>Core.Agent.AdminIE7.css</CSS_IE7>
                <JavaScript>Core.Agent.Admin.SysConfig.js</JavaScript>
            </Loader>
        </FrontendModuleReg>
    </Setting>
</ConfigItem>
                

Here we have a module (the admin overview page of the agent interface) which has special JavaScript, normal CSS (tagname <CSS>) and special CSS for Internet Explorer 7 (tagname <CSS_IE7>). All of these need to be loaded in addition to the common JavaScript and CSS defined for the agent interface.

It is also possible to specify module-specific CSS for Internet Explorer 8 (tagname <CSS_IE8>) and, in the case of the customer interface, for Internet Explorer 6 (tagname <CSS_IE6>).

There is one special case: for ToolbarModules, you can also add custom CSS files. Just add a CSS, CSS_IE7 or CSS_IE8 attribute to the configuration like this:

<ConfigItem Name="Frontend::ToolBarModule###410-Ticket::AgentTicketEmail" Required="0" Valid="1">
    <Description Translatable="1">Toolbar Item for a shortcut.</Description>
    <Group>Ticket</Group>
    <SubGroup>Frontend::Agent::ToolBarModule</SubGroup>
    <Setting>
        <Hash>
            <Item Key="Module">Kernel::Output::HTML::ToolBarLink</Item>
            <Item Key="Name">New email ticket</Item>
            <Item Key="Priority">1009999</Item>
            <Item Key="Link">Action=AgentTicketEmail</Item>
            <Item Key="Action">AgentTicketEmail</Item>
            <Item Key="AccessKey">l</Item>
            <Item Key="CssClass">EmailTicket</Item>
            <Item Key="CSS">OTRS.Agent.CustomToolbarModule.css</Item>
            <Item Key="CSS_IE7">OTRS.Agent.CustomToolbarModule.IE7.css</Item>
        </Hash>
    </Setting>
</ConfigItem>
                

Templating Mechanism

Internally, OTRS uses a templating mechanism to dynamically generate its HTML pages (and other content), while keeping the program logic (Perl) and the presentation (HTML) separate. Typically, a frontend module will use an own template file, pass some data to it and return the rendered result to the user.

The template files are located at: $OTRS_HOME/Kernel/Output/HTML/Standard/*.dtl

Inside of these templates, a set of commands for data manipulation, localization and simple logical structures can be used. This section describes these commands and shows how to use them in templates.

Template Commands

Data Manipulation Commands

In templates, dynamic data must be inserted, quoted etc. This section lists the relevant commands to do that.

$Data{""}

If data parameters are given to the templates by the application module, these data can be output to the template. $Data is the most simple, but also most dangerous one. It will insert the data parameter whose name is specified inside of the {""} into the template as it is, without any HTML quoting.

Warning

Because of the missing HTML quoting, this can result in security problems. Never output data that was input by a user without quoting in HTML context. The user could - for example - just insert a <script> tag, and it would be output on the HTML page generated by OTRS.

Whenever possible, use $QData{""} (in HTML) or $LQData{""} (in Links) instead.

Example: Whenever we generate HTML in the application, we need to output it to the template without HTML quoting, like <select> elements, which are generated by the function Layout::BuildSelection in OTRS.

<label for="Dropdown">Example Dropdown</label>
$Data{"DropdownString"}
                    

$QData{""}

This command has the same function as $Data{""}, but it performs HTML quoting on the data as it is inserted to the template.

The name of the author is $QData{"Name"}.
                    

It's also possible specify a maximum length for the value. If, for example, you just want to show 8 characters of a variable (result will be "SomeName[...]"), use the following:

The first 20 characters of the author's name: $QData{"Name","20"}.
                    

$LQData{""}

This command has the same function as $Data{""}, but it performs URL encoding on the data as it is inserted to the template. This should be used to output single parameter names or values of URLs, to prevent security problems. It cannot be used for complete URLs because it will also mask =, for example.

<a href="$Env{"Baselink"};Location=$LQData{"File"}">$QData{"File","110"}</a>
                    

$Env{""}

Inserts the environment variable with the name specified in {""}. Some examples:

The current user name is: $Env{"UserFirstname"}

Some other common predefined variables are:

$Env{"SessionID"} --> the current session id
$Env{"Time"} --> the current time e. g.  Thu Dec 27 16:00:55 2001
$Env{"CGIHandle"} --> the current CGI handle e. g.  index.pl
$Env{"UserCharset"} --> the current site charset e. g.  iso-8859-1
$Env{"Baselink"} --> the baselink --> index.pl?SessionID=...
$Env{"UserFirstname"} --> e. g. Dirk $Env{"UserFirstname"}
$Env{"UserLogin"} --> e. g. mgg@x11.org
$Env{"UserIsGroup[users]"} = Yes --> user groups (useful for own links)
$Env{"UserIsGroup[admin]"} = Yes $Env{"Action"} --> the current action
                    

Warning

Because of the missing HTML quoting, this can result in security problems. Never output data that was input by a user without quoting in HTML context. The user could - for example - just insert a <script> tag, and it would be output on the HTML page generated by OTRS.

Whenever possible, use $QEnv{""} instead.

$QEnv{""}

Works like $Env{""}, but performs HTML encoding when the data is inserted to the template.

The current user name is: $QEnv{"UserFirstname"}
                    

$Config{""}

With this tag you can insert config variables into the template. Let's see an example Kernel/Config.pm:

[Kernel/Config.pm]
    # FQDN
    # (Full qualified domain name of your system.)
    $Self->{FQDN} = 'otrs.example.com';
    # AdminEmail
    # (Email of the system admin.)
    $Self->{AdminEmail} = 'admin@example.com';
[...]
                    

To output values from it in the template, use:

The hostname is '$Config{"FQDN"}'
The admin email address is '$Config{"AdminEmail"}'
                    

Warning

Because of the missing HTML quoting, this can result in security problems.

Whenever possible, use $Quote{"$Config{""}"}.

$Quote{""}

This tag can be used to perform quoting on HTML strings, when no other quoting is possible.

$Quote{"$Config{"ProductName"}"} ($Quote{"$Config{"Ticket::Hook"}"})
                    

It's also possible specify a maximum length for the value. If, for example, you just want to show 8 characters of a variable (result will be "Some lon[...]"), use the following:

$Quote{"Some long long string", "8"})
                    

Localization Commands
$Text{""}

Translates the enlosed string into the current user's selected language and performs HTML quoting on the resulting string. If no translation is found, the original string will be used.

Translate this text: $Text{"Help"}
                    

When translating data coming from the application, use $Data inside of $Text, not $QData, to prevent double quoting:

Translate data from the application: $Text{"$Data{"Type"}"}
                    

You can also specify parameters (%s) inside of the string which should be replaced with other data:

Translate this text and insert the given data: $Text{"Change %s settings", "$Data{"Type"}"}
                    

$JSText{""}

Works in the same way as $Text{""}, but does not perform HTML encoding but JavaScript string escaping instead (all ' characters will be encoded as \'. So with the help of this tag you can make sure that even dynamic strings will not break your JavaScript code.

window.alert('$JSText{"Some message's content"}');

// after the command was replaced in the template, this will
// result in (for an English speaking agent):

window.alert('Some message\'s content');
                    

Make sure to use ' as string delimiter for strings where you want to use $JSText inside.

$TimeLong{""}

Inserts a localized date/time stamp (including a possible time zone difference of the current agent).

In different cultural areas, different convention for date and time formatting are used. For example, what is the 01.02.2010 in Germany, would be 02/01/2010 in the USA. $Time{""} abstracts this away from the templates. Let's see an example:

# from AgentTicketHistory.dtl
$TimeLong{"$Data{"CreateTime"}"}

# Result for US English locale:
06/09/2010 15:45:41
                    

First, the data is inserted from the application module with $Data. Here always an ISO UTC timestamp (2010-06-09 15:45:41) must be passed as data to $TimeLong{""}. Then $TimeLong{""} will take that data and output it according to the date/time definition of the current locale.

The data passed to $TimeLong{""} must be UTC. If a time zone offset is specified for the current agent, it will be applied to the UTC timestamp before the output is generated.

$TimeShort{""}

Works like $TimeLong{""}, but does not output the seconds.

$TimeShort{"$Data{"CreateTime"}"}

# Result for US English locale:
06/09/2010 15:45
                    

$Date{""}

Works like $TimeLong{""}, but outputs only the date, not the time.

$Date{"$Data{"CreateTime"}"}

# Result for US English locale:
06/09/2010
                    

Template Processing Commands
Comment

The dtl comment starts with a # at the beginning of a line and will not be shown in the html output. This can be used both for commenting the DTL (=Template) code or for disabling parts of it.

# this section is temporarily disabled
# <div class="AsBlock">
#     <a href="...">link</a>
# </div>
                    

$Include{""}

Includes another template file into the current one. The included file may also contain template commands.

# include Copyright.dtl
$Include{"Copyright"}
                    

dtl:block

With this command, one can specify parts of a template file as a block. This block needs to be explicitly filled with a function call from the application, to be present in the generated output. The application can call the block 0 (it will not be present in the output), 1 or more times (each with possibly a different set of data parameters passed to the template).

One common use case is the filling of a table with dynamic data:

<table class="DataTable">
    <thead>
        <tr>
            <th>$Text{"Name"}</th>
            <th>$Text{"Type"}</th>
            <th>$Text{"Comment"}</th>
            <th>$Text{"Valid"}</th>
            <th>$Text{"Changed"}</th>
            <th>$Text{"Created"}</th>
        </tr>
    </thead>
    <tbody>
<!-- dtl:block:NoDataFoundMsg -->
        <tr>
            <td colspan="6">
                $Text{"No data found."}
            </td>
        </tr>
<!-- dtl:block:NoDataFoundMsg -->
<!-- dtl:block:OverviewResultRow -->
        <tr>
            <td><a class="AsBlock" href="$Env{"Baselink"}Action=$Env{"Action"};Subaction=Change;ID=$LQData{"ID"}">$QData{"Name"}</a></td>
            <td>$Text{"$Data{"TypeName"}"}</td>
            <td title="$QData{"Comment"}">$QData{"Comment","20"}</td>
            <td>$Text{"$Data{"Valid"}"}</td>
            <td>$TimeShort{"$QData{"ChangeTime"}"}</td>
            <td>$TimeShort{"$QData{"CreateTime"}"}</td>
        </tr>
<!-- dtl:block:OverviewResultRow -->
    </tbody>
</table>
                    

The surrounding table with the header is always generated. If no data was found, the block NoDataFoundMsg is called once, resulting in a table with one data row with the message "No data found."

If data was found, for each row there is one function call made for the block OverViewResultRow (each time passing in the data for this particular row), resulting in a table with as many data rows as results were found.

Let's look at how the blocks are called from the application module:

 my %List = $Self->{StateObject}->StateList(
    UserID => 1,
    Valid  => 0,
);

# if there are any states, they are shown
if (%List) {

    # get valid list
    my %ValidList = $Self->{ValidObject}->ValidList();
    for ( sort { $List{$a} cmp $List{$b} } keys %List ) {

        my %Data = $Self->{StateObject}->StateGet( ID => $_, );
        $Self->{LayoutObject}->Block(
            Name => 'OverviewResultRow',
            Data => {
                Valid => $ValidList{ $Data{ValidID} },
                %Data,
            },
        );
    }
}

# otherwise a no data found msg is displayed
else {
    $Self->{LayoutObject}->Block(
        Name => 'NoDataFoundMsg',
        Data => {},
    );
}
                    

Note how the blocks have both their name and an optional set of data passed in as separate parameters to the block function call. Data inserting commands inside a block always need the data provided to the block function call of this block, not the general template rendering call.

For details, please refer to the documentation of Kernel::Output::HTML::Layout on dev.otrs.org.

dtl:js_on_document_complete

Marks JavaScript code which should be executed after all CSS, JavaScript and other external content has been loaded and the basic JavaScript initialization was finished. Again, let's look at an example:

<form title="$Text{"Move ticket to a different queue"}" action="$Env{"CGIHandle"}" method="get">
    <input type="hidden" name="Action" value="AgentTicketMove"/>
    <input type="hidden" name="QueueID" value="$QData{"QueueID"}"/>
    <input type="hidden" name="TicketID" value="$QData{"TicketID"}"/>
    <label for="DestQueueID" class="InvisibleText">$Text{"Change queue"}:</label>
    $Data{"MoveQueuesStrg"}
</form>
<!-- dtl:js_on_document_complete -->
<script type="text/javascript">
    $('#DestQueueID').bind('change', function (Event) {
        $(this).closest('form').submit();
    });
</script>
<!-- dtl:js_on_document_complete -->
                

This snippet creates a small form and puts an onchange-Handler on the <select> element which causes and automatical form submit.

Why is it neccessary to enclose the JavaScript code in dtl:js_on_document_complate? Starting with OTRS 3.0, JavaScript loading was moved to the footer part of the page for performance reasons. This means that within the <body> of the page, no JavaScript libraries are loaded yet. With dtl:js_on_document_complete you can make sure that this JavaScript is moved to a part of the final HTML document, where it will be executed only after the entire external JavaScript and CSS content has been successfully loaded and initialized.

Inside the dtl:js_on_document_complete block, you can use <script> tags to enclose your JavaScript code, but you do not have to do so. It may be beneficial because it will enable correct syntax highlighting in IDEs which support it.

Using a template file

Ok, but how to actually process a template file and generate the result? This is really simple:

# render AdminState.dtl
$Output .= $Self->{LayoutObject}->Output(
    TemplateFile => 'AdminState',
    Data         => \%Param,
);
            

In the frontend modules, the Output() function of Kernel::Output::HTML::Layout is called (after all the needed blocks have been called in this template) to generate the final output. An optional set of data parameters is passed to the template, for all data inserting commands which are not inside of a block.