Development How-Tos

This collection provides in-depth guidance on various aspects of ILIAS development.

Tabs

Development How-Tos

Welcome to our ILIAS Development How-Tos! This collection provides in-depth guidance on various aspects of ILIAS development. Covering essential topics such as the development process, as well as guidance on how to customize system styles, write unit tests or handle permissions, this page aims to equip you with practical knowledge and best practices including ready-to-use code snippets.

General Development

The description of the development process of ILIAS has become part of our new devguide tutorial, see here.

 

Want to Contribute? Great!

Table of Contents

  1. Who is a contributor?
  2. How to contribute?
    1. Pull Request to the Repositories
      1. Rules for Contributors
      2. List of Labels
      3. Looking for Shepherd
      4. Rules for Community Members assigned to PRs
    2. Want to Contribute Something else than Commits?

Who is a contributor?

In general we consider everyone who takes part in the development of ILIAS a contributor, where the contribution could take various forms, e.g. testing, creating feature request, writing documentation, reporting security issues. We aim to include everyone performing these or similar activities in our processes.

For practical reasons we need to define a contributor to be everyone who wants to contribute commits to our repository for now. We trying to figure out ways to also include Testers, Translators, Authors and other people into the processes described here. If you want to contribute to said activities please have a look here.

As a contributor you will be named in the release notes of our major releases with your name and your organisation as we find them in our commit history and in your profile on GitHub. If your don't want to be listed, please write a short mail to the Technical Board of the ILIAS society.

How to contribute?

Pull Request to the Repositories

Pull requests (PRs) without assignee will be assigned by the Technical Board (TB) to the appropriate community member. The TB will also help to resolve problems with PRs and associated processes, if you require mediation, please write a comment mentioning via the Technical Board (@ILIAS-eLearning/technical-board) in the discussion of the PR.

Please make yourself acquainted with the ILIAS Society's process for functional feature requests before starting to create your PR. Your PR should thus only contain bug fixes or non-functional changes to our code base.

Rules for Contributors

We are happy that you want to contribute. To enable us to merge your PRs in our code please make sure:

  • that your PR has a description that tells what is changed and why - with a size relative to the changes
  • that your PRs is minimal - prefer to make two small PRs instead of one big PR
  • that you discuss huge PRs with the responsible authorities in advance - this will save your time if the authorities do not agree with your proposed change
  • that you create commits of self-contained logical units with concise commit messages and no unnecessary whitespace - this will help reviewers to understand what you did
  • that your code is understandable and is documented - this will help reviewers as well
  • that your commit follows the ILIAS coding guidelines - this is a bare minimum when it comes to style that we require for new code
  • you don't introduce new code violations which could have been easily found by importing and running our PhpStorm PHP Inspection Profile
  • that your are approachable for questions of reviewers

If your PR contains a bugfix please reference the number of the mantis ticket in the title 12345 - To many spaces, link the ticket in the description and label the ticket with bugfix. You may make one PR per affected branch.

Please label non-bugfix PRs as improvement.

Please be prudent with PRs that are work in progress or are request for comments. Only open these if you strictly want some feedback from the greater developer community for a concrete proposal of some code or some guideline. Do not use PRs to the ILIAS-repository as a general working space for incomplete features or ideas. Prefer other measures like workshops or VCs for discussion about ideas or approaches. If you are positive that you definitely need to open a PR as a draft, prefix the summary with "WIP -"" for these kind of PRs to prevent them from being merged accidentally.

List of Labels

Currently, the following labels are used for Pull-Requests. These labels will be assigned by the Technical Board or Authorities:

Label Description
authorities The label authorities has to be assigned to PRs that contain updates to the authorities of a component.
bugfix PRs with the label bugfix propose a solution for a reported bug in the official Bugtracker https://mantis.ilias.de
dependencies The label dependecies is used for PRs which propose new or updated dependencies. Please don't forget to also add the label jour fixe, when proposing new dependencies.
documentation The label documentation has to be assigned to PRs adding or updating documentation.
improvement The label improvement is used for PRs which propose a general improvement of code or documentation which is not related to a bug.
javascript The label javascript has to be set for PRs changing Javascript code.
jour fixe PRs which should be discussed during the next Jour Fixe are labeled with this jour fixe. Please set this label at least 2 days before the envisaged date of Jour Fixe.
kitchen sink All contributions to the Kitchen Sink Project are labeled accordingly.
Looking for Shepherd The label Looking for Shepherd has to be set for PRs which changes made for unmaintained components.
php The label php has to be set for PRs changing PHP code.
roadmap The label roadmap is assigned to PRs that contain strategical or tactical discussions of technical topics regarding the future of a component.
technical board This label is given for PRs which will be discussed in a meeting of the Technical Board. The label will be removed after the discussion.

Looking for Shepherd

Looking for Shepherd is a label in GitHub to mark PRs made for unmaintained components. As there is no developer that gets assigned such bugs due to her/his authority, pull requests with this tag can be reviewed by every ILIAS developer who can commit and decide if it is accepted and merged. We kindly ask every ILIAS developer to look into these PRs regularly and take responsibility for our shared code base.

Rules for Community Members assigned to PRs

As an FOSS community, we should be glad that people want to contribute code to our project as this reflects usage of our project. To show this when handling PRs, please make sure

  • that you react to every PR assigned to you within 21 days - at least with a thank you and a target date if your schedule is tight
  • that you give at least a brief statement why you close a PR if you reject one
  • that you merge the changes in the PR in other branches if required

Want to Contribute Something else than Commits?

We are happy to get contributions that are no commits as well. There are many other things you could contribute to ILIAS:

  • Ideas for new Features: The development of ILIAS is driven by requirements from the community. Contribute your ideas via feature requests.
  • Bug Reports: We do our best, but ILIAS might contain bugs we do not know about yet. Check out how the ILIAS Community handles bug reports.
  • Information about Security Issues: Check out how the ILIAS community handles security issues. The Reporter of security issues will also be named in the release notes.
  • Time for Testing or Testcases: We always need people who contribute testcases and carry them them before new releases. Please have a look here (German only). If you have questions, do not hesitate to contact our test case manager Fabian Kruse (fabian@ilias.de).

 

Developer Mode

Sometimes features that are not implemented completely need to be committed to CVS because several programmers work on the same source code or new features or objects should be tested on dependencies to other parts of ILIAS. For these cases we have created the possibility to set parts of the system or whole object types into developer mode.

Object types or functions in developer mode are not shown in ILIAS by default. So users are not affected with features that are not working properly yet.

Activating Objects

To display functions in developer mode please add the following line in your client.ini in the [system] section:

DEVMODE = "1"

Now you will see everything as usual.

If you delete the line or set the value to 0 you will disable developer mode again.

How to Use DEVMODE for Programming

To hide entire object types by setting them to DEVMODE just add a new attribute to the desired object in objects.xml, e.g.:

<object name="grp" class_name="Group" checkbox="1" inherit="1" translate="0" devmode="1">

In this case group objects will now not longer appear in the system.

To hide particular code parts or functions use the new constant DEVMODE:

if (DEVMODE) 
    {
        // do something only if devmode is activated
    }

 

Active Records

How to use Active Record in ILIAS

Active Records

General

The ActiveRecord-Implementation in ILIAS should help developers to get rid of multiple developments of CRUD functionality in their model classes. A lot of redundant code is to be found in ILIAS due to the implementations of read- and write processes to the persistent layer. ILIAS-ActiveRecord provides a lot of useful helpers such as a QueryBuilder, dynamic CRUD, ObjectCaching und data source maintenance.

Differences against other implementations

ActiveRecord are well known in other frameworks and languages such as Ruby, .NET, CakePHP … Most ActiveRecords directly represent the persistent layer, mostly database-tables. Changes in the database are automatically represented by the model. Modifications on the class-members are only possible by modifying the database-field. This is the the only big difference between ILIAS-ActiveRecord and other ActiveRecord-Implementations. ILIAS-ActiveRecord describes the whole class-member in PHP-Code with the information for the persistent layer (such as data-type, length, …). Advantages of this implementation:

  • The class-member is represented in your PHP-class and not just dynamically loaded, you ‘see’ your members and let IDEs like PHPStorm automatically implement your setters and getters.
  • You ‘see’ directly the field-attributes of your member in the persistent layer. Information about your members can be accessed by field-classes.

Implement your ActiveRecord-Class

Structure of your model

An ActiveRecord-Class normally extends from the abstract ActiveRecord. Let us use the following example: “We need a Message-Model. A Message has a title, a body, a sender and a receiver. Additionally the Message can be of the priority ‘low’, ‘normal’ or ‘high’ and can have a status like ‘new’ and ‘read’.” Our Model could look like this:

<?php
require_once('./Customizing/global/plugins/Libraries/ActiveRecord/class.ActiveRecord.php');
require_once(dirname(__FILE__) . '/../../Connector/class.arConnectorSession.php');

/**
 * Class arMessage
 *
 * @author  Fabian Schmid <fs@studer-raimann.ch>
 * @version 1.0.0
 */
class arMessage extends ActiveRecord {

        const TYPE_NEW = 1;
        const TYPE_READ = 2;
        const PRIO_LOW = 1;
        const PRIO_NORMAL = 5;
        const PRIO_HIGH = 9;

        const TABLE_NAME = 'ar_message';

        /**
         * @return string
         */
        static function returnDbTableName() {
                return self::TABLE_NAME;
        }

        /**
         * @var int
         *
         * @con_is_primary true
         * @con_sequence true
         * @con_has_field  true
         * @con_fieldtype  integer
         * @con_length     8
         */
        protected $id;
        /**
         * @var string
         *
         * @con_has_field true
         * @con_fieldtype text
         * @con_length    256
         */
        protected $title = '';
        /**
         * @var string
         *
         * @con_has_field true
         * @con_fieldtype clob
         * @con_length    4000
         */
        protected $body = '';
        /**
         * @var int
         *
         * @con_has_field  true
         * @con_fieldtype  integer
         * @con_length     1
         */
        protected $sender_id = 0;
        /**
         * @var int
         *
         * @con_has_field  true
         * @con_fieldtype  integer
         * @con_is_notnull true
         * @con_length     1
         */
        protected $receiver_id = 0;
        /**
         * @var int
         *
         * @con_has_field  true
         * @con_fieldtype  integer
         * @con_length     1
         * @con_is_notnull true
         */
        protected $priority = self::PRIO_NORMAL;
        /**
         * @var int
         *
         * @con_has_field  true
         * @con_fieldtype  integer
         * @con_length     1
         * @con_is_notnull true
         */
        protected $type = self::TYPE_NEW;


        /**
         * @param mixed $body
         */
        public function setBody($body) {
                $this->body = $body;
        }


        /**
         * @return mixed
         */
        public function getBody() {
                return $this->body;
        }


        /**
         * @param int $priority
         */
        public function setPriority($priority) {
                $this->priority = $priority;
        }


        /**
         * @return int
         */
        public function getPriority() {
                return $this->priority;
        }


        /**
         * @param int $receiver_id
         */
        public function setReceiverId($receiver_id) {
                $this->receiver_id = $receiver_id;
        }


        /**
         * @return int
         */
        public function getReceiverId() {
                return $this->receiver_id;
        }


        /**
         * @param int $sender_id
         */
        public function setSenderId($sender_id) {
                $this->sender_id = $sender_id;
        }


        /**
         * @return int
         */
        public function getSenderId() {
                return $this->sender_id;
        }


        /**
         * @param string $title
         */
        public function setTitle($title) {
                $this->title = $title;
        }


        /**
         * @return string
         */
        public function getTitle() {
                return $this->title;
        }


        /**
         * @param int $type
         */
        public function setType($type) {
                $this->type = $type;
        }


        /**
         * @return int
         */
        public function getType() {
                return $this->type;
        }
}

?>

The class implements the public static Method ‘returnDbTableName’, which returns the identifier of the container in the persistent layer. The rest of the Class are Members, Setters and Getters. This Class is fully functional, no other methods have to be implemented to have full CRUD, Caching, Collections, Factory, … All Class-Members, which should be represented in the persistent layer, are additionally documented with PHPDoc.

Using CRUD

After implementing your modelclass you can use the ActiveRecord CRUD commands to build and modify objects of the class:

$arMessage = new arMessage();
$arMessage->setTitle('Hello World');
$arMessage->setBody('Development using ActiveRecord saves a lot of time');
$arMessage->create();
// OR
$arMessage = new arMessage(3);
echo $arMessage->getBody();
// OR
$arMessage = new arMessage(6);
$arMessage->setType(arMessage::TYPE_READ);
$arMessage->update();
// OR
$arMessage = arMessage::find(58); // find() Uses the ObjectCache
$arMessage->delete();

Fields and FieldList

An ActiveRecord-Class-Member is described with the following attributes in PHPDoc. This information is used to provide a proper persistent layer access.

Attribute-Name Description Possible Values
con_hasfield Defines whether the field is represented in the persistent layer or not (false doesn’t has to be written) true/false
con_is_primary Member is primary key. Only one primary for one class possible. true/false
con_sequence The (primary-)field has an auto-increment. This is needed in most of the cases true/false
con_is_notnull Is member not_null (as in MySQL) true/false
con_fieldtype All ilDB-Field-Types are currently supported text, integer, float, date, time, timestamp, clob
con_length Length of the field in the persistent layer determines from the fieldtype. See 'Databse Access and Database Schema' for further information.

All this information is parsed from the PHPDoc once per ActiveRecord-Class and request and are cached for all other instances of this type. So there should not be a remarkable performance-drop. This is an Example for a primary key $id:

/**
 * @var int
 *
 * @con_is_primary true
 * @con_has_field  true
 * @con_sequence  true
 * @con_fieldtype  integer
 * @con_length     8
 */
protected $id;

All the meta information can be access in the ActiveRecord:

public function dummy() {
        echo $this->arFieldList->getPrimaryField();
        echo $this->arFieldList->getFieldByName('title')->getFieldType();
        echo $this->getPrimaryFieldValue();
}

ActiveRecordList

Basics

The ActiveRecordList-Class represents the Collection, Repository, ... The List is accessible through the ActiveRecord or in an own instance:

/**
 * @return arMessage[]
 * @description a way to get all objects is to call get() directly on your class
 */
public static function getAllObjects() {
        $array_of_arMessages = arMessage::get();

        // OR

        $arMessageList = new arMessageList();
        $array_of_arMessages = $arMessageList->get();
        return $array_of_arMessages;
}

Both examples return an Array of arMessage-Objects. But The List provide more functionality, such as a QueryBuilder:

public function getSome() {
        $array_of_arMessages = arMessage::where(array('type' => arMessage::TYPE_READ))->orderBy('title')->get();

        // OR

        $arMessageList = new arMessageList();
        $arMessageList->where(array('type'=> arMessage::TYPE_READ));
        $arMessageList->orderBy('title');
        $array_of_arMessages = $arMessageList->get();
}

Build a query

Where

Method-Call Query
arMessage::where(array('type' => arMessage::TYPE_READ)); SELECT * FROM ar_message WHERE ar_message.type = 1
arMessage::where(array('type'=>arMessage::TYPE_NEW), '!='); SELECT * FROM ar_message WHERE ar_message.type != 1
arMessage::where(array( 'type' => arMessage::TYPE_NEW, 'title' => '%test%' ), '='); SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title = '%test%'
arMessage::where(array( 'type' => arMessage::TYPE_NEW, 'title' => '%test%' ), array( 'type' => '=', 'title' => 'LIKE' )); SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title LIKE '%test%'
arMessage::where(array( 'type' => arMessage::TYPE_NEW ))->where(array( 'title' => '%test%' ), 'LIKE') SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title LIKE '%test%'

Oder By

Method-Call Query
arMessage::orderBy('title'); SELECT * FROM ar_message ORDER BY title ASC
arMessage::orderBy('title', 'DESC'); SELECT * FROM ar_message ORDER BY title DESC
arMessage::orderBy('title', 'DESC')->orderBy('type'); SELECT * FROM ar_message ORDER BY title DESC, type ASC'

Limit

Method-Call Query
arMessage::limit(0, 100); SELECT * FROM ar_message LIMIT 0, 100

Join

Method-Call Query
arMessage::innerjoin('usr_data', 'receiver_id', 'usr_id'); SELECT ar_message.*, usr_data.* FROM ar_message INNER JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id
arMessage::leftjoin('usr_data', 'receiver_id', 'usr_id', array('email')); SELECT ar_message.*, usr_data.email FROM ar_message LEFT JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id

Combining statements

Method-Call Query
arMessage::innerjoin('usr_data', 'receiver_id', 'usr_id'); SELECT ar_message.*, usr_data.* FROM ar_message INNER JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id
arMessage::leftjoin('usr_data', 'receiver_id', 'usr_id', array('email')); SELECT ar_message.*, usr_data.email FROM ar_message LEFT JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id

Get information

get(); arMessage::orderBy('title')->get(); will return an array of arMessage-Object.

getArray(); arMessage::orderBy('title')->getArray(); will return a 2D record-value array. getArray() can be filtered or the index oft he array can be set: arMessage::getArray(NULL, array('title')); will return an array with only the titles of the records.

getCollection(); If you build a query using the statements explaines above, you can store this Collection for further use. arMessage:: getCollection(); return the ActiveRecordList-Object with all statements saves.

first(); Returns the first object from your query.

last(); Returns the last object from your query.

Use ActiveRecord for Sorting and Filters in Tables

When using an Activerecord for presentation in ilTableGUI, this is an Example to use external sorting and external segmentation, which will increase performance on larger tables:

protected function parseData() {
        $this->determineOffsetAndOrder();
        $this->determineLimit();
        $arMessageList = arMessage::orderBy($this->getOrderField(), $this->getOrderDirection());
        foreach ($this->filter as $field => $value) {
                if ($value) {
                        $arMessageList->where(array( $field => $value ));
                }
        }
        $this->setMaxCount($arMessageList->count());
        $arMessageList->limit($this->getOffset(), $this->getLimit());
        $arMessageList->orderBy('title'); // Secord order field
        $arMessageList->dateFormat('d.m.Y - H:i:s'); // All date-fields come in three ways: formatted, unix, unformatted (as in db)
        $this->setData($arMessageList->getArray());
}

Connector

ILIAS ActiveRecord uses the ilDB connection as default persistent layer. A connector is responsible for all connections to the persistent layer. It’s possible to write your own Connector, an example is delivered with the ActiveRecord. It uses the User-Session to store the objects. This connector is not fully functional (there is no Querybuilder). Use the abstract arConnector Class to implement your own connector.

Maintenance of data source

ActiveRecord allows to maintain your persistent layer like the ILIAS Database for your class. There is no need to install the database on your own, ActiveRecord can install und update the table: Please do not use installDB; and updateDB; for core-development. Using them will be reported as a bug.

Generate DB-Update-Step to install your Class

Use the already known DB-Update-Steps to generate your AR-Databases. There is a Helper-Script to auto-generate a Installation-Updatestep. Implement these two lines with your ActiveRecord somewhere in ILIAS-Code and run the site. It generates and Downloads a php-file with the installation-Step:

$arBuilder = new arBuilder(new arMessage());
$arBuilder->generateDBUpdateForInstallation();

The downloaded file can be used to install the appropriate db-table.

You can use these methods to delete or truncate your table even in dbupdate-Scripts:

arMessage::resetDB(); // Truncates the Database
$ilDB->dropTable(arMessage::TABLE_NAME, false); // Deletes the Database

It's not yet possible to generate e database-modification step with this feature. Please write those as usual and don't forget to represent your changes in your AR-based Class.

Generate Class-File from existing MySQL-Table (Beta)

It’s possible to generate a PHP-Classfile for an existing MySQL-Datatable, e.g. with the Table usr_data:

$arConverter = new arConverter('usr_data', 'arUser');
$arConverter->downloadClassFile();

Object-Cache

Every ActiveRecord is being cached, developers don’t have to mind this task. The cache is updated on every object modification and is deleted after deleting the object. The object cache storage can be accessed if necessary:

// e.g.

$arMessageFour = new arMessage(4);
arObjectCache::purge($arMessageFour);
if (! arObjectCache::isCached('arMessage', 4)) {
        arObjectCache::store(new arMessage(4));

        return arObjectCache::get('arMessage', 4);
}

 

Exceptions

Using Exceptions

If code identifies any problems that prevent the normal processing from being executed, e.g. a directory is not writable even if it should be, exceptions should be thrown. This usually happens within the application layer. Upper contexts that have called the application classes (e.g. GUI or SOAP) can act appropriate. E.g. the user interface layour can present the error using ilUtil::sendFailure.

Defining new Exceptions

  • New exceptions should be implemented as classes derived from ilException (found in component ILIAS/Exceptions).
  • These class file should be put into a subdirectory exceptions within the component directory, e.g. components/ILIAS/AdvancedEditing/exceptions.
  • If a component uses exceptions a top exception named after the component should be used. Other exceptions classes should be derived from this class, e.g. ILIAS/AdvancedEditing/exceptions/class.ilAdvancedEditingException.php.
<?php
/* Copyright (c) 1998-2011 ILIAS open source, Extended GPL, see docs/LICENSE */

require_once 'components/ILIAS/Exceptions/classes/class.ilException.php'; 

/** 
 * Class for advanced editing exception handling in ILIAS. 
 * 
 * @author Michael Jansen <mjansen@databay.de>
 * @version $Id$ 
 * 
 */
class ilAdvancedEditingException extends ilException
{
    /** 
     * Constructor
     * 
     * A message is not optional as in build in class Exception
     * 
     * @param   string $a_message message
     */
    public function __construct($a_message)
    {
        parent::__construct($a_message);
    }
}
?>

Throwing and Catching Exceptions

Throwing an exception:

function update()
{
    if (!$this->getId())
    {
        throw new ilObjectException('No id given');
    }
}

Catching an exception (in this case in the GUI layer):

try
{
    $this->object->setTitle($title);
    $this->object->update();
    ilUtil::sendSuccess($this->lng->txt('saved_settings'));
    return true;
}
catch (ilObjectException $e)
{
    ilUtil::sendFailure($e->getMessage());
    return false;
}

Exception Wrapping

Advantages of exception wrapping are:

  • Exception wrapping avoids the breaking of layer abstractions
  • Exception wrapping gives layers the possibility to add context informations to the exception
Exception Wrapping I (application class):
...
    try 
    {
        $parser = new ilSaxParser($this->xml);
        $parser->parse();
        ...
    {
    catch (ilSaxParserException $e)
    {
        throw new ilObjectException(?Cannot parse object XML: ?.e->getMessage());
    }
...

Exception Wrapping II (here: GUI class):

...
    try 
    {
        $this->object->import();
        lUtil::sendSuccess($this->$lng('obj_created'));
    }
    catch(ilObjectException $e)

 

System Styles

System Styles

The templates folder of ILIAS contains the ILIAS System Styles. System Styles are defined by the set of icons, fonts, html templates and CSS/SCSS files that define the visual appearance of ILIAS. They differ from Content Styles, which enable to manipulate the classes defining the appearance of user generated content.

Custom Styles

System Styles may be customized by creating custom System Styles. Custom styles have to be placed in the ./public/Customizing/skin directory to be active. One may have multiple substyles which may be active for different branches of the repository.

A GitHub Repo for a custom System Style based on the default Delos will be available soon.

Tools

You may directly use the Sass version shipped with ILIAS.

If you want to use your own version, first install the necessary tools to your server. These tools include nodejs and the node packet manager. After that you can install the sass compiler that is used to turn SCSS into CSS using:

npm install -g sass

or

Download Dart SASS from Github and add it to the machine's PATH.

Access available Styles through Frontend

  1. Navigate to "Administration -> Layout and Styles" of you ILIAS Installation.
  2. In a table you see all available System Styles.
  3. You may assigne users to styles via Actions Dropdown
  4. You may set Sub Styles for certain sections of the repository via Actions Dropdown

How-To

Step 1: Create skin directory

To create a new skin, first add a new subdirectory to directory ./public/Customizing/skin, e.g. ./public/Customizing/skin.

In the future, we will provide a base System Style based on the default Delos that you can download from Github and place here.

Step 2: Create template.xml File

One file that must exist in every skin is the file template.xml. E.g. ./public/Customizing/skin/myskin/template.xml:

<?xml version = "1.0" encoding = "UTF-8"?>
<template xmlns = "http://www.w3.org" version = "1" name = "MySkin">
        <style name = "MyStyle" id = "mystyle" image_directory = "images"/>
</template>

Every skin can contain multiple styles. This example defines one style called MyStyle. This skin/style combination will be listed as MySkin/MyStyle in the ILIAS Style and Layout administration. The ILIAS administration is the place where you can activate/deactivate styles, and where you can assign users from one skin to another.

Step 3: Create main CSS File

The id attribute of the style tag defines the name of the corresponding style sheet (CSS) file. This CSS file must also be added to the skin directory (here: ./public/Customizing/skin/myskin/mystyle/mystyle.css). You should start with a copy of the default CSS file located at templates/default/delos.css. The best way to see which styles are used on a given ILIAS screen is to open the HTML source of the screen. Probably import delos in the top line of your css like so: @import url("../../../../assets/css/delos.css");

If your CSS file contains references to (background) images, these images must be present at their defined locations. If you copied the default CSS file, the image paths will not be correct anymore. You can either copy them to your skin directory, change the CSS definitions or provide your own image files.

Step 3: Better Alternative

To have a working directory for your skin, you can also copy the complete folder templates/default of your ilias installation to a new folder below ./public/Customizing/skin/skin within that directory, edit the file template.xml to have an unique Style Name and id. This is needed to identify the new skin in ILIAS' administration. Then copy the standard delos.css file to "your-id.css". Take care: the main CSS-File must reflect the id in its name (see above).

However, best use the stand alone skin delos git repo, which is always an up-to-date copy of the delos skin from the main repo. Clone it into your ./public/Customizing/skin folder, make your changes and keep it always up-to-date for fixes from the main repo.

Step 3: Sass

Do not froget to re-compile the scss-file after each change. Switch to the delos scss folder and execute:

./node_modules/sass/sass.js delos.scss mystyle.css

or

./node_modules/sass/sass.js --style=compressed delos.scss mystyle.css

for a minified CSS version.

Step 4: Add Icons (Optional)

If you want to replace the default icons coming with ILIAS, you can add new representations of them to your skin. They must be stored in a subdirectory named like the image_directory attribute of the style tag in the template.xml file.

E.g. if you want to replace the default icon for categories public/assets/images/standard/icon_cat.sfg, and your template file defines image_directory = "images" as in the example above, the new version must be stored as ./public/Customizing/skin/myskin/mystyle/images/icon_cat.svg.

Step 5: Change Layout (Optional)

The layout is specified in HTML template files. Some standard default template files can be found in directory templates/default. Other template files are stored within subdirectories of the Modules or Services directories. Most ILIAS screens use more than one template file. Some template files are reused in many ILIAS screens (e.g. the template file that defines the layout of the main menu).

To replace a template file for your skin, you have to create a new one in your skin directory. Please note, that your skin should only contain template files that are modified. You do not need to copy all default template files to your new skin.

Since ILIAS 5.3 we move aim to move most of the UI towards the UI Components. They are located in src/UI. To overwrite those you need to add the respective tpl files in your skins folder.

Examples: * components related template files must be stored in a similar subdirectory structure (omit the templates subdirectory). E.g. to replace the template file components/ILIAS/XYZ/templates/tpl.xyz.html create a new version at ./public/Customizing/skin/myskin/components/ILIAS/XYZ/tpl.xyz.html. A template of a UI Component located in src/UI/templates/default/XYZ/tpl.xyz.html can be customized by creating a ./public/Customizing/skin/myskin/UI/XYZ/tpl.xyz.html file.

The following list contains some standard template files, that are often changed in skins:

  • Standard Layout: components/ILIAS/UI/templates/default/Layout/tpl.standardpage.html, the frame of the DOM for the complete ILIAS page. Also checkout the according scss variable under section Layout (UI Layout Page).
  • Meta Bar: components/ILIAS/UI/templates/default/MainControls/tpl.metabar.html, the Bar on the top holding Notification, Search User Avatar, etc. Also checkout the according metabar scss variables.
  • Main Bar: components/ILIAS/UI/templates/default/MainControls/tpl.mainbar.html, the Bar on the left holding triggers for opening the slates for accessing Repository, Dasbhoard etc. Content. Also checkout the according mainbar scss variables.
  • Slate: components/ILIAS/UI/templates/default/MainControls/Slate/tpl.slate.html, the Slates triggered by opening items of the Main Bar. Also checkout the according slate scss variables.
  • Breadcrumbs: components/ILIAS/UI/templates/default/Breadcrumbs/tpl.breadcrumbs.html, Breadcrumbs working as locator on the top of the page. Also checkout the breadcrumb scss variables.

  • Startup Screens (Login, Registration, ...): components/ILIAS/Init/templates/default/tpl .startup_screen.html

Step 6: Change the ILIAS Icon

The main ILIAS icon is stored in the images Directory as logo/HeaderIcon.svg. You can replace this easyly by your own Icon in svg format. As long as your Icon is close to a square, this may be all that is needed. Probably you want to change the file favicon .ico in ILIAS' root directory too. For non-square Icons you may refer to:

Installation and Maintenance » Change the ILIAS icon

Migration

There might be changes you need to consider if updating to a new ILIAS version.

Note that this changelog was introduced for ILIAS 5.3. If migrating to a lower version you might find helpful information by consulting:

Installation and Maintenance » Prepare for a new skin

ILIAS 10

  • Important: The location of the skin was moved to ./public/Customizing/skin
  • System style Management through GUI has been abandoned, see: https://docu.ilias.de/go/wiki/wpage_1_1357
  • Sass is no shipped with NPM, see: https://github.com/ILIAS-eLearning/ILIAS/pull/8115

ILIAS 9

A proposal for better structuring the System Styles has been provided and accepted by the JF in 2021, see: https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/src/UI/docu/sass-guidelines.md

With ILIAS 9 the SCSS as been restructered according to the ITCSS structure suggested by this proposal, and the depencency to less from Bootstrap has mostly been removed. However, the change from less to SCSS and the abandonment from Bootstrap means, that System Styles from 8 and lower are NOT compatible with ILIAS 9. They can not be imported, be used, or compiled.

However, note, that most of the css should still work. Also less and scss are not that far appart. Best read through our SCSS Coding Guidelines to get started.

ILIAS 7

The icon-font-path for glyphs has been renamend to il-icon-font-path and the location has changed due to a move from the bootstrap library to the new location for external libraries. The new location is: "../../../../node_modules/bootstrap/fonts/". If a 5.2 style is imported, the variable icon-font-path must be adapted accordingly.

In March 2022, we moved the general Test & Assessment CSS (ta.css and ta_split.css) to less/Modules/Test/delos.less (like other CSS for modules) to start refactoring this module's style code. As part of this change, the override mechanism that fetches a custom style for just the T&A has been removed. Please use the standard skin setup described in this document to style the Test & Assessment like the rest of your custom skin.

ILIAS 6

Major parts of the UI of ILIAS 6 have changed. It is therefore recommended, to create a new skin for ILIAS think an manually move changes that are still needed from oder versions to the new skin.

Also, most importantly the following components have been introduced:

  • Standard Layout, template directory: src/UI/templates/default/Layout, the frame of the DOM for the complete ILIAS page. Also checkout the according scss variable under section Layout (UI Layout Page).
  • Meta Bar template directory: src/UI/templates/default/MainControls, the Bar on the top holding Notification, Search User Avatar, etc. Also checkout the according metabar scss variables.
  • Main Bar template directory: src/UI/templates/default/MainControls, the Bar on the left holding triggers for opening the slates for accessing Repository, Dasbhoard etc. Content. Also checkout the according mainbar scss variables.
  • Slate template directory: src/UI/templates/default/MainControls/Slate, the Slates triggered by opening items of the Main Bar. Also checkout the according slate scss variables.
  • Breadcrumbs template directory: src/UI/templates/default/Breadcrumbs, Breadcrumbs working as locator on the top of the page. Also checkout the breadcrumb scss variables.

See above section on information on how to customize those components.

ILIAS 5.3

The icon-font-path for glyphs has changed due to a move from the bootstrap library to the new location for external libraries. The new location is: "../../../../libs/bower/bower_components/bootstrap/fonts/". If a 5.2 style is imported, the variable icon-font-path must be adapted accordingly.

Coding Guidelines

If you want to change and contribute ILIAS style code, please refer to the most recent SCSS Coding Guidelines

 

Template Engine

Template Engine

As a component developer you should rarely need to use the Template Engine directly since you should use the components of the UI framework whenever possible.

But if you e.g. need to implement UI components yourself you need to understand the basics of the template engine.

The main idea of using templates is the separation of style (css), layout (views) and PHP code (controllers). All layout information is stored in .html template files. These files contain HTML markup and placeholders that are dynamically replaced by controller code.

Placeholders and Blocks

[...]
<!-- BEGIN address -->
<div class="ilProfileSection">
    <h3 class="ilProfileSectionHead"></h3>
    <!-- BEGIN address_line -->
    <div></div>
    <!-- END address_line -->
</div>
<!-- END address -->
[...]

This example contains placeholders and and the block definition for address_line and address.

$tpl = new ilTemplate("tpl.address.html", true, true, components/ILIAS/User");
$tpl->setCurrentBlock("address_line");
foreach($address as $line)
{
    if(trim($line))
    {
        $tpl->setVariable("TXT_ADDRESS_LINE", trim($line));
        $tpl->parseCurrentBlock();
    }
}
$tpl->setCurrentBlock("address");
$tpl->setVariable("TXT_ADDRESS", $lng->txt("address"));
$tpl->parseCurrentBlock();

$html = $tpl->get()

To fill a template you need an instance of ilTemplate. Start iterating inner blocks (setCurrentBlock(), parseCurrentBlock()) and fill their placeholders using setVariable(). Work subsequently through all blocks from inner to outer blocks.

To render the content of your template you need to call $tpl->get().

Now you want to include your HTML in the main ILIAS layout.

Main Template

The UI framework supports you with a global main template which supports methods to include output on an ILIAS screen.

$tpl = new ilTemplate("tpl.address.html", true, true, components/ILIAS/User");
[...]

// get main template
$main_tpl = $DIC->ui()->mainTemplate();

// set content (center column)
$main_tpl->setContent($tpl->get());

// set title
$main_tpl->setTitle($title);

// set title icon
$main_tpl->setTitleIcon(ilUtil::getImagePath("standard/icon_cat.gif"));

// set description section
$main_tpl->setDescription($description);

// set content of right column
$main_tpl->->setRightContent($right_content_html);

 

Unit Tests

Writing Unit Tests

ILIAS supports unit testing with the PHPUnit testing framework. We highly recommend their excellent documentation that explains the basic ideas of unit testing, how PHPUnit works and how it is installed.

Configuration

After installing PHPUnit on your machine you need to configure ILIAS to work with it. The test cases are performed with an authenticated user in your ILIAS installation. The configuration determines which user is used to perform the test cases. You will find a configuration file template at:

Services/PHPUnit/config/cfg.phpunit.template.php

Make a copy at:

Services/PHPUnit/config/cfg.phpunit.php

Now change the file and provide a valid client ID, account ID, and username.

Please activate the developer mode in your client.ini.php when running PHPUnit tests.

Test Cases

Test classes should be located in a subdirectory test of your components directory.

[components/ILIAS]/[ComponentName]/test

E.g. the test classes for the Administration service are located at ILIAS/Administration/test. The names for test class files should usually be derived from the application class they are written for.

[ApplicationClassName]Test.php

E.g. if you write test cases for a class ilSetting the test class file should be named ilSettingTest.php.

The class should contain a method starting with "test" for each test case.

<?php
/*
    +-----------------------------------------------------------------------------+
    | ILIAS open source                                                           |
    +-----------------------------------------------------------------------------+
    | Copyright (c) 1998-2006 ILIAS open source, University of Cologne            |
[...]
    +-----------------------------------------------------------------------------+
*/

class ilSettingTest extends PHPUnit_Framework_TestCase
{
    protected $backupGlobals = FALSE;

    protected function setUp()
    {
        include_once("./Services/PHPUnit/classes/class.ilUnitUtil.php");
        ilUnitUtil::performInitialisation();
    }

    public function testSetGetSettings()
    {
        $set = new ilSetting("test_module");
        $set->set("foo", "bar");
        $value = $set->get("foo");

        $this->assertEquals("bar", $value);
    }
[...]
}
?>
  • Your test class must be derived from class PHPUnit_Framework_TestCase.
  • Your test must be executable with PHPUnit 5.7.
  • If your test needs an ILIAS Installation, it must be annotated with @group needsInstalledILIAS, and...
    • you must set $backupGlobals to false, otherwise, your test cases will not work.
    • the setUp() method should contain the ilUnitUtil::performInitialisation(); call. ilUnitUtil can be found in Services/PHPUnit/classes.
  • If your test does not need an ILIAS Installation, it must not be annotated with @group needsInstalledILIAS and also should not use ilUnitUtil::performInitialisation().

To run your test class you simply call phpunit in your ILIAS root directory with the local path of your test class (omit the .php suffix):

> phpunit components/ILIAS/Administration/test/ilSettingTest

  1. Please write your test cases in a way that the requirements to run them are minimal. The test cases should run on a usual system. They should not require that certain conditions are given on the test system, e.g. an empty repository.
  2. Clean up the data that is written during the test case. If possible, a test run should not leave any data created during the test in the system.

Test Suites

Test suites allow to perform aggregated tests. They should be provided on the component level, one test suite for each service and module. The file name must follow the format: il[components/ILIAS][ComponentName]Suite.php

E.g. the test suite class for the Administration service is located at: ILIAS/Administration/test/ilServicesAdministrationSuite.php

The class is named: ilServicesAdministrationSuite

<?php
/*
    +-----------------------------------------------------------------------------+
    | ILIAS open source                                                           |
    +-----------------------------------------------------------------------------+
    | Copyright (c) 1998-2009 ILIAS open source, University of Cologne            |
[...]
    +-----------------------------------------------------------------------------+
*/

class ilServicesAdministrationSuite extends PHPUnit_Framework_TestSuite
{
    public static function suite()
    {
        $suite = new ilServicesAdministrationSuite();

        // add each test class of the component     
        include_once("./components/ILIAS/Administration/test/ilSettingTest.php");
        $suite->addTestSuite("ilSettingTest");
        [...]

        return $suite;
    }
}
?>

The example outlines the basic structure of a test suite class. To run the test suite, simply pass the class name to phpunit in the ILIAS main directory: > phpunit components/ILIAS/Administration/test/ilServicesAdministrationSuite

The Global Test Suite

The global test suite scans all Services and Modules directory automatically for component level test suites and aggregates them to one big test suite. The suite is located in the PHPUnit Service. You can run the suite by typing: > phpunit Services/PHPUnit/test/ilGlobalSuite

The global suite will first list all component suites that are included in execution. If your suite is not listed, you should check whether your suite file and class is following the naming conventions as written in the previous section.

 

UI-Framework

The ILIAS UI-Framework

The ILIAS UI-Framework helps you to implement GUIs consistent with the guidelines of the Kitchen Sink.

Tutorial

You find a tutorial on what UI Components are and on their purpose, how they are used and what you should consider while using them here.

Correctness by Construction and Testability

The design of the ILIAS UI-Framework makes it possible to identify lots of guideline violations during the construction of a GUI and turn them into errors or exceptions in PHP. This gives you the freedom to care about your GUI instead of the guidelines it should conform to. You also can check your final GUI for Kitchen Sink compliance using the procedures the framework provides for Unit Testing.

Condsider the overarching UX

When building views with the UI framework or adding new components to it, please be mindful of the larger context. Every UI and their elements should follow an overarching strategy to intuitively guide the user as much as possible to the functions and information they are currently interested in. Coherent and comprehensive UI concepts make ILIAS easier to use and avoid overcrowded and confusing screens.

Here are some points to consider when creating a layered, context-sensitive UX strategy: * Anticipate the user intent to group, highlight, show, and hide information and actions depending on context. * Make use of the experience a user brings from other apps to match a mental model they have already learned. * Consider giving operations their own specialized interface, step, or mode rather than building a one-size-fits-all screen but utilize common UX-concepts and already existing UI components when you do.

Documents with recommendations on how to approach UX challenges of specific UI components and use cases will be added below: * Best practices for properties and actions displayed on repository objects

Implementing Elements in the Framework

To get a brief overview on how to proceed before starting to implement elements in the UI framework, we recommend to read our DevGuide about this topic.

How to Implement a Component?

If you would like to implement a new component for the framework, you should perform the following tasks:

  1. Add your new component into the respective factory interface. E.g. if you introduce a component of a completely new type, you MUST add the description to the main factory (components/ILIAS/UI/src/Factory.php). If you add a new type of button, you MUST add the description to the existing factory for buttons, located at src/UI/Component/Button/Factory.
  2. The description MUST use the following template:

    /**
    * ---
    * description:
    *   purpose: What is to be done by this control
    *   composition: What is this control composed of
    *   effect: What happens if the control is operated
    *   rivals:
    *     Rival 1: What other controls are similar, what is their distinction
    *
    * background: Relevant academic information
    * context: 
    *     - The context states: where this control is used specifically with examples (this list might not be complete) and how common is this control used
    *
    * rules:
    *   usage:
    *     1: Where and when an element is to be used or not.
    *   composition:
    *     1: How this component is to be assembled.
    *   interaction:
    *     1: How the interaction with this object takes place.
    *   wording:
    *     1: How the wording of labels or captions must be.
    *   style:
    *     1: How this element should look like.
    *   ordering:
    *     1: How different elements of this instance are to be ordered.
    *   responsiveness:
    *     1: How this element behaves on changing screen sizes.
    *   accessibility:
    *     1: How this element is made accessible.
    *
    * ---
    * @param   string $content
    * @return \ILIAS\UI\Component\Demo\Demo
    **/
    public function demo($content);
    
  3. This freshly added function in the factory leads to an error as soon as ILIAS is opened, since the implementation of the factory (located at src/UI/Implementation/Factory.php) does not implement that function yet. For the moment, implement it as follows:

    /**
    * @inheritdoc
    */
    public function demo($content)
    {
        throw new \ILIAS\UI\NotImplementedException();
    }
    
  4. Next, you should think about the interface you would like to propose for this component. You need to model the component you want to introduce by defining its interface and the factory method that constructs the component. To make your component easy to use, it should be creatable with a minimum of parameters and use sensible defaults for the most of its properties. Also think about the use cases for your component. Make typical use cases easy to implement and more special use cases harder to implement. Put getters for all properties on your interface. Make sure you understand, that all UI components should be immutable, i.e. instead of defining setters setXYZ you must define mutators withXYZ that return copies of your component with changed properties. Try to use as little mutators as possible and try to make it easy to maintain the invariants defined in your rules when mutators will be used. Take care to keep it as minimal as possible. Add a description for each function. For the demo component, this interface could look as follows (located at (src/UI/Component/Demo/Demo.php):

    <?php declare(strict_types=1)
    namespace ILIAS\UI\Component\Demo;
    
    /**
     * Interface Demo
     * @package ILIAS\UI\Component\Demo
     */
    interface Demo extends \ILIAS\UI\Component\Component {
    
        /**
         * Gets the content of this demo component
         * @return Demo
         */
        public function getContent();
    }
    
  5. Make sure all tests are passing by executing '''phpunit tests/UI'''. For the demo component this means we have to add the following line to NoUIFactory in UI/test/Base.php:

    public function demo($demo){}
    
  6. Congratulations, at this point you are ready to present your work to the JF. Create a PR named "UI NameOfTheComponent". To make it easy for non-developers to follow the discussion, you MUST link to the changed/added factory classes and mock in the description you provide for your PR. Further, it would be wise to enhance your work with a little mockup. This makes it much easier to discuss the new component at the JF. So best create such an example and also link it in your comment, e.g. at src/UI/examples/Demo/mockup.php:

    <?php declare(strict_types=1)
    function mockup() {
        return "<h1>Hello Demo!</h1>";
    }
    

    If needed, you can also add JS-logic (e.g. src/UI/examples/Demo/mockup.php):

    <?php declare(strict_types=1)
    function script() {
        return "<script>console.log('Hello Demo');</script>Open your JS console!";
    }
    

    However best might be to just provide a screenshot showing what the component will look like:

    function mockup() {
        global $DIC;
        $f = $DIC->ui()->factory();
        $renderer = $DIC->ui()->renderer();
    
        $mockup = $f->image()->responsive("src/UI/examples/Demo/mockup.png");
        return $renderer->render($mockup);
    }
    
  7. Next you should create the necessary tests for the new component. Since this is a very important step, it deserved its own chapter below. Make sure you write at at least tests for all interface methods and one full rendering test.

  8. Currently you will only get the NotImplementedException you threw previously. That needs to be changed. First, add an implementation for the new interface (add it at src/UI/Implementation/Component/Demo/Demo.php):

    <?php declare(strict_types=1)
    namespace ILIAS\UI\Implementation\Component\Demo;
    
    use ILIAS\UI\Component\Demo as D;
    use ILIAS\UI\Implementation\Component\ComponentHelper;
    
    class Demo implements D\Demo {
        use ComponentHelper;
    
        /**
         * @var string
         */
        protected $content;
    
        /**
         * @param $content
         */
        public function __construct($content){
            $this->checkStringArg("title", $content);
    
            $this->content = $content;
        }
    
        /**
         * @inheritdoc
         */
        public function getContent(){
            return $this->content;
        }
    }
    
  9. Next, make the factory return the new component (change demo() of src/UI/Implementation/Factory.php):

    return new Component\Demo\Demo($content);
    
  10. Then, implement the renderer at src/UI/Implementation/Component/Demo/Demo.php:

    <?php declare(strict_types=1)
    
    /* Copyright (c) 2016 Timon Amstutz <timon.amstutz@ilub.unibe.ch> Extended GPL, see docs/LICENSE */
    
    namespace ILIAS\UI\Implementation\Component\Demo;
    
    use ILIAS\UI\Implementation\Render\AbstractComponentRenderer;
    use ILIAS\UI\Renderer as RendererInterface;
    use ILIAS\UI\Component;
    
    class Renderer extends AbstractComponentRenderer {
        /**
         * @inheritdocs
         */
        public function render(Component\Component $component, RendererInterface $default_renderer): string 
        {
            // if this is not our component, call cannotHandleComponent($component)
            // to throw a unified exception. 
            if (!$component instanceof Component\Demo\Demo) {
                $this->cannotHandleComponent($component);
            }
    
            $tpl = $this->getTemplate("tpl.demo.html", true, true);
            $tpl->setVariable("CONTENT",$component->getContent());
            return $tpl->get();
        }
    }
    
  11. Finally you need the template used to render your component. Create it at src/UI/templates/default/Demo/tpl.demo.html: html <h1 class="il-demo"></h1>
  12. Execute the UI tests again. At this point, everything should pass. Thanks, you just made ILIAS more powerful!
  13. Remember to add examples demonstrating the usage of your new component. Those examples should showcase the key features of the new component. Note that they will be used as basis for the test cases in testrail (see next point). The example for the demo looks as follows (located at src/UI/examples/Demo/render.php):

      <?php declare(strict_types=1)
      function render() {
          //Init Factory and Renderer
          global $DIC;
          $f = $DIC->ui()->factory();
          $renderer = $DIC->ui()->renderer();
    
          $demo = $f->demo("Demo rendered by template!");
    
    
          return $renderer->render($demo);
      }
    
  14. Remember to adapt/add the Test Cases in Testrail section UI Components so that a tester with no technical expertise can confirm that all examples work as intended. They must be available and linked to the PR before the PR will be merged.

  15. Optional: You might need to add some scss, to make your new component look nice. However, only do that if this is really required. (see: SCSS Guidelines ).

  16. Formulate at least one test-case for your new component on testrail.ilias.de](https://testrail.ilias.de). Best, try to formulate a testcase for each relevant client-side interaction. E.g. if your component contains a button that triggers a modal on-click, write a test-case for this interaction. Post the the link to this test-case in a comment/the description of your PR.
  17. Optional: If your component introduces a new factory, do not forget to wire it up in the according location of the initialisation. Have a look into ilInitialisation::initUIFramework in components/ILIAS/Init/class/class.ilInitialisation.php.

How to write unit tests for a Component?

When creating a new component, please make sure you provide at least tests for all interface methods and one full rendering test. For the demo component we have created above, this looks as follows (located at UI/tests/Component/Demo/DemoTest.php). Please make sure your unit test extends from the ILIAS_UI_TestBase, so you can use functionalities like getting the test renderer for rendering tests.

    <?php declare(strict_types=1)

    require_once(__DIR__."/../../../../vendor/composer/vendor/autoload.php");
    require_once(__DIR__."/../../Base.php");

    use \ILIAS\UI\Component as C;

    /**
     * Test on demo implementation.
     */
    class DemoTest extends ILIAS_UI_TestBase {
        public function testImplementsFactoryInterface() {
            $f = new \ILIAS\UI\Implementation\Factory();

            $this->assertInstanceOf("ILIAS\\UI\\Factory", $f);
            $demo = $f->demo("Demo Implementation!");
            $this->assertInstanceOf( "ILIAS\\UI\\Component\\Demo\\Demo", $demo);
        }

        public function testGetContent() {
            $f = new \ILIAS\UI\Implementation\Factory();
            $demo = $f->demo("Demo Implementation!");

            $this->assertEquals("Demo Implementation!", $demo->getContent());
        }

        public function testRenderContent() {
            $r = $this->getDefaultRenderer();
            $f = new \ILIAS\UI\Implementation\Factory();
            $demo = $f->demo("Demo Implementation!");


            $html = $r->render($demo);

            $expected_html = '<h1 class="il-demo">Demo Implementation!</h1>';

            $this->assertHTMLEquals($expected_html, $html);
        }
    }

If you are implementing or adjusting the unit tests for a more complex component, you need to be careful when writing rendering tests. If your component features further components from the framework, either by composable aspects like e.g. providing a roundtrip modals action buttons (ILIAS\UI\Component\Modal\Roundtrip::withActionButtons()), or by rendering further components during the rendering process, it is implicitly coupled to the HTML of other components.

If thats the case, you MUST implement your rendering tests using "component stubs", to fully decouple the unit tests from other components. This means, instead of rendering actual components in your unit test, you provide mocked instances of the component interfaces used within your component or unit test, so we have full control over the HTML being rendered for each component. An implementation for the first scenario (externally provided components) would look like this for our demo component:

// ...

class DemoTest extends ILIAS_UI_TestBase
{
    // ...

    /**
     * Tests if the action button which is provided is catually rendered.
     */
    public function testWithActionButtonRendering(): void
    {
        $f = new \ILIAS\UI\Implementation\Factory();
        $demo = $f->demo('');

        // create the component mock for a standard button.
        $button_stub = $this->createMock(\ILIAS\UI\Component\Button\Standard::class);

        // configure the mock to return our desired HTML, it is advised to make this
        // value unique, so we can check only the existense using a str_contains()
        // approach.
        $button_html = sha1(\ILIAS\UI\Component\Button\Standard::class);
        $button_stub->method('getCanonicalName')->willReturn($button_html);

        // make the mock known to the renderer, so it can be rendered.
        $renderer = $this->getDefaultRenderer(null, [$button_stub]);

        // provide the mock to the method we are testing.
        $demo = $demo->withActionButton($button_stub);

        $actual_html = $renderer->render($demo);

        // checks only the existence of the component, using str_contains() to
        // search our unique stub HTML.
        $this->assertTrue(str_contains($actual_html, $stub_html));

        $expected_html = <<<EOT
<div class="c-demo">
    <h1 class="il-demo"></h1>
    $button_html
</div>
EOT;

        // checks the exact position of the component stub, using the unique HTML
        // value embeded in the expected HTML.
        $this->assertEquals(
            $this->brutallyTrimHTML($expected_html), 
            $this->brutallyTrimHTML($actual_html)
        );
    }
}

Note it is also important to think about what you are actually testing with your rendering test. If testing only for the existence of something is important, you can use the str_contains() approach to reduce maintenance in the future. However, it might be necessary to also test the location of a ceratin component, e.g. if an action button should be inside a certain container, so the styling works properly. In this case, you may use the assertEquals() approach with the embedded stub-HTML.

For the latter scenario (internally rendered components), you need to make the component stubs available in the UI factory which is injected into the actual renderer. This way, we still have full control over the HTML of other components which may be rendered and checked in your rendering tests. For our demo component this would work like this:

// ...

class DemoTest extends ILIAS_UI_TestBase
{
    // ...
    protected \ILIAS\UI\Component\Button\Factory $button_factory;
    protected \ILIAS\UI\Component\Button\Standard $button_stub;
    protected string $button_html;

    /** Sets up the button stub which will be rendered internally */
    public function setUp() : void
    {
        // setup the button stub similar to the previous example.
        $this->button_stub = $this->createMock(\ILIAS\UI\Component\Button\Standard::class);
        $this->button_html = sha1(\ILIAS\UI\Component\Button\Standard::class);
        $this->button_stub->method('getCanonicalName')->willReturn($this->button_html);

        // setup the factory so it will return the button stub.
        $this->button_factory = $this->createMock(\ILIAS\UI\Component\Button\Factory::class);
        $this->button_factory->method('standard')->willReturn($this->button_stub);

        // don't forget to call our parent!
        parent::setUp();
    }

    /** Overrides the factory retrieval so it uses our instance of the button factory. */
    public function getUIFactory(): NoUIFactory
    {
        return new class ($this->button_factory) extends NoUIFactory {
            public function __construct(
                protected \ILIAS\UI\Component\Button\Factory $button_factory,
            ) {
            }

            public function button(): \ILIAS\UI\Component\Button\Factory
            {
                return $this->button_factory;
            }
        };
    }

    /** Tests if the action button is rendered properly during the internal rendering process. */
    public function testInternalActionButtonRendering(): void
    {
        $f = new \ILIAS\UI\Implementation\Factory();
        $demo = $f->demo('');

        // render the component making the stub available.
        $renderer = $this->getDefaultRenderer(null, [$this->button_stub]);
        $actual_html = $renderer->render($demo);

        // check existence ... 
        $this->assertTrue(str_contains($actual_html, $this->stub_html));

        $expected_html = <<<EOT
<div class="c-demo">
    <h1 class="il-demo"></h1>
    $this->button_html
</div>
EOT;

        // check location ...
        $this->assertEquals(
            $this->brutallyTrimHTML($expected_html), 
            $this->brutallyTrimHTML($actual_html)
        );
    }
}

How can I make my component look different in some context?

For some use cases you might get to the point where you want to know where your component is rendered to emit different HTML in your renderer. A general idea of the UI framework is that components have their unique look that is recognisable throughout the system, which is the exact reason you could not find a simple way to get to know where your component is being rendered.

There still might be circumstances where a context dependent rendering is indeed required. A context can be understood as a collection or stack of all other surrounding UI components. This means, if e.g. a Page component is rendered which needs to render a Dropdown component somewhere that features some further Shy button compoennt, the rendering stack or context when the button is rendered would be "Page -> Dropdown -> Shy". The DefaultRenderer orchestrates this process and is responsible to remember this context at any time during the entire rendering process. Component renderers are able to react to this context using a RendererFactory, which receives the current context as an argument when loading the renderer of some component. The FSLoader contains directions on how to introduce new renderers for different contexts in your component.

Before using this mechanism, please consider if you really require a different look in a different context and, and if thats the case, whether you could achieve the same effect using CSS or not.

How to Change an Existing Component?

  1. Create a new branch based on the current trunk.
  2. Implement your changes and create a PR on the current trunk.
  3. Clearly state in the description of the PR, why you believe the change to be necessary. If your change fixes a bug, link to the according bugfix. If you need the change for implementing a feature, link the Feature Request. If you are changing the interface of a component, your proposal will be discussed in the next JF (see: rules ).

Abstraction of Javascript in the Framework

Note: The concept described in this section is not yet fully finalized.

The functionality of some components relies on Javascript. As an example, a modal is shown and closed by clicking on some triggerer component. As a user of the framework, you should not worry about writing the Javascript logic for this kind of interaction.

About Triggerables, Triggerer and Signals

Before describing the concept in more detail, you should be aware of the following definitions: * Signal A signal describes a Javascript action of a component which can be triggered by another component of the framework. * Triggerable A component offering some signals that can be triggered by other components. * Triggerer A component triggering a signal of another component. * Event The Javascript event on which a signal is being triggered, e.g. click, hover etc.

Again, consider the example if a user opens a modal by clicking on a button:

  • Signal: Show Modal
  • Triggerable: Modal
  • Triggerer: Button
  • Event: Click

How to trigger Signals of Components

This code snippet shows how to open a modal by clicking on a button:

global $DIC;
$factory = $DIC->ui()->factory();
$modal = $factory->modal()->roundtrip('Title', $factory->legacy('Hello World'));
$button = $factory->button()->standard('Open Modal', '#')
  ->withOnClick($modal->getShowSignal());

The button is a triggerer component. As such, it offers the method withOnClick which takes any Signal offered by a triggerable. This is how the framework connects triggerer components with signals of triggerable components. Similar to the click event, there exist methods withOnHover and withOnLoad to abstract the Javascript events on which a signal is being triggered.

Attention: Immutable Objects and Signals

Each triggerer component stores the signals it triggers. By cloning a component, these signals are cloned as well. This means that a cloned component may trigger the same signals as the original. Consider the following example: php global $DIC; $factory = $DIC->ui()->factory(); $modal = $factory->modal()->roundtrip('Title', $factory->legacy('Hello World')); $button1 = $factory->button()->standard('Open Modal', '#') ->withOnClick($modal->getShowSignal()); $button2 = $button1->withLabel('Open the same Modal'); In the example above, $button2 will open the same modal as $button1. In order to reset any triggered signals, use the method $button2->withResetTriggeredSignals().

Implementing a Triggerer Component

Any component acting as triggerer must implement the Triggerer interface. This interface is further extended by interfaces describing the Javascript event on which a signal is being triggered. Currently, there exist the Clickable, Hoverable and Onloadable interfaces. Please check out the button component for an example implementation.

Implementing a Triggerable Component

Any component acting as triggerable must implement the Triggerable interface. In addition, it must offer at least one signal that can be triggered by other components. The renderer of the triggerable component is also responsible for executing the Javascript logic if any signal is getting triggered. Please check out the modal component for an example implementation. The next section explains how the concept of signals/triggerer/triggerable is abstracted in Javascript.

Technical Details

The magic how everything is glued together on the Javascript side happens in the renderers of the triggerer and triggerable components: * Triggerer: The renderer of the triggerer component knows which signals are triggered on which events. It registers a new event handler on the component (e.g. on click/hover) which will trigger the signal as a custom Javascript event. * Triggerable: The renderer of the triggerable component knows the signals and the Javascript logic which must be executed if any of the signals is getting triggered.

Each signal has a unique alphanumeric ID. The triggerer uses this ID to trigger a custom Javascript event which will be handled by some event handler from the triggerable. In order to understand this concept, take a look at the Javascript code that is getting generated by the renderers if a button opens a modal on click:

Renderer of button
The renderer of the button generates the HTML for the button AND registers the event handler for the button click. This event handler triggers a custom Javascript event with the same name as the ID of show signal of the modal:

html <button id="button1">Open Modal</button> <script> $('#button1').on('click', function() { $(this).trigger('id_of_signal_to_open_the_modal', { 'id' : 'id_of_signal_to_open_the_modal', 'triggerer' : $(this), 'event' : 'click', 'options' : {} } return false; }); </script> Note that some event data is passed with the event, such as the triggerer, event and event options. This allows the event handler of the modal to identify the triggerer.

Renderer of modal
The renderer of the modal generates the HTML for the modal AND registers an event handler on the ID of the show signal. The event handler calls some Javascript logic to show the modal.

<div class="modal" id="modal1"> ... </div>
<script>
$(document).on('id_of_signal_to_open_the_modal', function(event, signalData) { 
  il.UI.modal.showModal('modal1', signalData);
});
</script>

Note: signalData contains the event data passed by the triggerer, e.g. signalData.triggerer holds the JQuery object of the button.

For more information on events in Javascript in the context of JQuery: http://api.jquery.com/category/events/

Code Style

We are currently not enforcing code style, but eventually will.

PHP

Use PHPStan to check your files:

./scripts/PHPStan/run_check.sh src/UI/...

There are different levels of checks, you can e.g. run

./vendor/composer/vendor/bin/phpstan analyse --level 8 src/UI/...

to override ./scripts/PHPStan/phpstan.neon, however, level 9/max is the desired goal.

Java Script

In order to validate your JS-files, run

./node_modules/.bin/eslint --parser-options ecmaVersion:13 src/UI/templates/js/...

or change/add .eslintrc.json in ILIAS' root directory:

{
  "parserOptions": {
    "ecmaVersion": 13
  },
  "extends": "airbnb-base"
}

To install the linter (and its config), run

npm i -D "eslint" "eslint-config-airbnb-base" "eslint-plugin-import"

FAQ

There are so many rules, is that really necessary?

The current state of the art in ILIAS GUI creation was dubbed "The GUI Anarchy" by some smart person. The introduction of the ILIAS UI framework aims at bringing more structure in the GUIs of ILIAS. As one (or two) maintainers for all things GUI of ILIAS is no option for several reasons and the current state (without rules) is anarchy, rules seem to be the only sensible option to get some structure. All existing rules have a purpose, but there might be a more terse way to explain them. If you have found it, we'll be glad to accept your PR.

I don't understand that stuff, is there anyone who can explain it to me?

Yes. Ask Richard Klees richard.klees@concepts-and-training.de, Timon Amstutz timon.amstutz@ilub.unibe.ch or Thibeau Fuhrer thibeau@sr.solutions.

 

Services

AccessControl

Permission Handling

Reference IDs and Object IDs

All objects within the ILIAS repository are protected by the role-based access control system (RBAC). An object within the repository is identified by the so-called Reference-ID (ref_id). The Reference-ID determines the object and the position of the object within the repository tree. Objects are identified by their Object-ID. Every object has only one Object-ID but may be associated with multiple Reference-IDs if it is referenced in multiple locations within the repository tree.

Related Classes:

  • ilObject (classes/class.ilObject.php): Handles objects and their IDs
  • ilTree (classes/class.ilTree.php): Handles the repository tree (and other trees).

Related Tables:

  • object_data: Stores basic object data
  • object_reference: Stores Reference-IDs of objects
  • tree: Stores the repository tree

How to check the access permission of a user

The access checking is provided by the class ilAccessHandler. An instance of this class is globally available through the DI-Container. The most important method of this class is:

checkAccess($a_permission, $a_cmd, $a_ref_id, $a_type = "", $a_obj_id = "")
global $DIC;

$access = $DIC->access();
if ($access->checkAccess("write", "", $this->object->getRefId())

This method checks whether the current user may perform the action $a_cmd associated with the permission $a_permission on the repository object identified by $a_ref_id. The method checks the following things:

  1. RBAC Check: Check whether the current user has the permission $a_permission for the object identified by $a_ref_id. $a_permission may be, for example, "read" or "write".

  2. Repository Path Check: Checks whether the current user has read access to all parent nodes of the object identified by $a_ref_id. For example, if a learning module is located within a course A in category B, ILIAS checks read access of course A and category B.

  3. Condition Check: Checks whether the user fulfills all preconditions for the object. Preconditions could be defined by authors, administrators, or tutors of repository objects. They consist of a trigger, a target, and a condition expression. For example, Learning module A (target) can be accessed only if the user has passed (condition) Test B (trigger).

  4. Object Status Check: Checks whether the status of the object allows a command to be performed. For example, if a learning module is set to "offline," no read access-related command may be performed, even if the read permission is granted by RBAC.

The check of step 4 makes use of type-specific access classes. Every object type (learning modules, glossaries, chats, etc.) must provide an access class derived from ilObjectAccess, named ilObj_Type_Access, e.g., ilObjGlossaryAccess, ilObjLearningModuleAccess, ilObjChatAccess, etc. Those classes must contain a static method:

_checkAccess($a_cmd, $a_permission, $a_ref_id, $a_obj_id, $a_user_id = "")

This method should check the object status-related conditions and return true if everything is OK or false if not.

Related classes:

  • ilAccessHandler (components/ILIAS/AccessControl/classes/class.ilAccessHandler.php)
  • ilObjectAccess (classes/class.ilObjectAccess)

 

Calendar

How to handle dates in different time zones

With release 3.10.0 ILIAS introduces support for individual user time zones. On the presentation, side dates should always be displayed in the time zone of the currently logged in user. With the revision of the calendar in 3.10.0, new classes for the manipulation and presentation of dates have been introduced.

PHP Basics

There are three interesting levels of time:

  1. The Server Time Zone
  2. The 'Coordinated Universal Time' / UTC (which is more or less GMT)
  3. The User Time Zone

Working with PHP usually brings you in contact with times in the server time zone and, if you work with unix timestamps, implicitely with UTC.

Example 1: Getting current unix timestamp (which are seconds from 1.1.1970 in UTC)

// will display something like 1216474062
echo time();

Example 2: Getting the current server time zone (you will rarely need this).

// show current timezone, e.g. 'Europe/Berlin'
echo date_default_timezone_get();

Example 3: Using date() to format a date/time value. The PHP date function uses a (UTC) unix timestamp as input and delivers the date/time in the current server time zone.

// displays something like 2008-07-19 15:27:42 (current server time zone)
echo date('Y-m-d H:i:s', time());

Example 4: The opposite function of date() is mktime(), which delivers a (UTC) unix timestamp, based on date/time values provided in the current server time zone.

// get current hour from date (server time zone) and using the hour in mktime
// -> mktime input parameters must be in server time zone
$server_hour = date('H', time());
echo mktime($server_hour);

Up to ILIAS 3.9.x all string representations ("YYYY-MM-DD HH:MM:SS") are in server time zone, also when working with MySQL timestamps. Only integer representated unix timestamps (like delivered by time()) are seconds in UTC since 1.1.1970.

With version 3.10.0 ILIAS displays dates in a user defined time zone. Every user can choose the time zone in his or her personal settings.

Dates in the format of the user defined time zone are only used in the user interface. They must not be saved to the database.

Using ilDateTime objects in ILIAS

  1. Creating date objects:
// Creating a DateTime object (from unix timestamp)
$now = new ilDateTime(time(), IL_CAL_UNIX);

// Creating a DateTime object (from string in server time zone)
$date = new ilDateTime('2008-07-17 10:00:00', IL_CAL_DATETIME);

// Creating a Date object (no time, no time zone conversion)
$birthday = new ilDate('1972-04-04', IL_CAL_DATE);
  1. Manipulating dates:
$days = new ilDateTime(time(), IL_CAL_UNIX);

// plus one hour
    $days->increment(IL_CAL_HOUR, 1);

// minus two days
$days->increment(IL_CAL_DAY, -2);

// plus three months
$days->increment(IL_CAL_MONTH, 3);

...
  1. Comparing dates:
$early = new ilDateTime('2008-07-17 06:00:00', IL_CAL_DATETIME);
$late = new ilDateTime('2008-07-17 23:00:00', IL_CAL_DATETIME);

// Check equality => returns false
ilDateTime::_equals($early,$late);

// Check same day => returns true
ilDateTime::_equals($early, $late, IL_CAL_DAY);

// Check $early < $late
ilDateTime::_before($early, $late);

// Check $early > $late
ilDateTime::_after($early, $late);

...
  1. Converting date formats:
// Creating object from date/time string representation in server time zone
$today = new ilDateTime('2008-07-17 12:00:00', IL_CAL_DATETIME);

// Convert to unix timestamp
$unix = $today->get(IL_CAL_UNIX);

// Convert to custom format in server time zone (PHP date syntax)
$date_str = $today->get(IL_CAL_FKT_DATE, 'YmdHis');

// Convert to datetime in UTC time zone (usually you will not need this)
$utc = $today->get(IL_CAL_DATETIME, '', 'UTC');

Storing dates in the database

MySQL offers mainly three possibilities for storing dates in the database: Timestamp, DateTime or Integer Fields. Timestamp and DateTime are used similarly, but Timestamp internally converts to UTC and DateTime does not.

It is recommended to not rely on the database for timezone conversions, and instead convert all datetimes to UTC manually before persisting them. That leaves two options for storing datetimes: as a fromatted string in UTC in a datetime field, or as a unix timestamp in an integer field.

  1. Using field type 'datetime':

You provide a normal string representation in UTC (YYYY-MM-DD HH:MM:SS). When reading you will also get a string representation in UTC.

// Update/Insert
$registration_start = new ilDateTime(time(), IL_CAL_UNIX);
$db->manipulate(
    "UPDATE grp_settings SET" .
    " registration_start = " . $db->quote($start->get(IL_CAL_DATETIME), ilDBConstants::T_DATETIME) .
    " WHERE obj_id = " . $db->quote(123, ilDBConstants::T_INTEGER)
);

// Select
$set = $db->query("SELECT * FROM grp_settings ");
while ($record = $db->fetchAssoc($set)) {
    $registration_start = new ilDateTime($record['registration_start'], IL_CAL_DATETIME);
}
  1. Using field type 'integer' (for unix timestamps):

The (UTC) unix timestamps are just put into an integer field.

// Update/Insert
$registration_start = new ilDateTime('2008-11-11 11:11:11', IL_CAL_DATETIME);
$db->manipulate(
    "UPDATE grp_settings SET" .
    " registration_start = " . $db->quote($start->get(IL_CAL_UNIX), ilDBConstants::T_INTEGER) .
    " WHERE obj_id = " . $db->quote(123, ilDBConstants::T_INTEGER)
);

// Select
$set = $db->query("SELECT * FROM grp_settings");
while ($record = $db->fetchAssoc($set)) {
    $registration_start = new ilDateTime($record['registration_start'], IL_CAL_UNIX);
}

Date Presentation

For the presentation of dates in the user interface, the individually selected user time zone must be respected. This is done by the ilDatePresentation class.

  1. Presentation of dates (without time):
ilDatePresentation::formatDate(
   new ilDate(time(), IL_CAL_UNIX)
);

// Returns: 
// 12. Jul 2008
  1. Presentation of dates (with time):
ilDatePresentation::formatDate(
    new ilDateTime(time(), IL_CAL_UNIX)
);

// Returns:
// 12. Jul 2008 1:01pm or
// 12. Jul 2008 13:01
// depending on the user time zone and hour format setting.
  1. Presentation of date periods:
ilDatePresentation::formatPeriod(
    new ilDateTime(time(), IL_CAL_UNIX),
    new ilDateTime(time() + 7200, IL_CAL_UNIX)
);

// Returns:
// 17. Jul 2008 1:00 pm - 3:00 pm or
// 17. Jul 2008 23:00 - 18. Jul 2008 01:00
// Depending on the user time zone and hour format settings.

Date-Inputs in Forms

For date/time form fields, use the Kitchen Sink Date Time input. Make sure to manually configure the component such that it respects the user settings concerning time zone and date-time format.

 

Cron

Cron

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Table of Contents * Implementing and Configuring a Cron-Job * Providing a Cron-Job * ilCronJob * ilCronJobResult * Schedule * Settings * Customizing * Misc * Cron Job Execution * Permission Context

Implementing and Configuring a Cron-Job

To give more control of if and when cron-jobs are executed to administrators a 2nd implementation of cron-jobs has been added to ILIAS 4.4+. All existing cron-jobs have been migrated and thus moved to their respective modules and services. The top-level directory "cron/" will probably be kept because of cron.php but should otherwise be empty at some point.

Providing a Cron-Job

A module or service has to "announce" its cron-jobs to the system by adding them to their respective module.xml or service.xml.

  • The job id has to be unique.
  • An optional path can be added if the module/service directory layout differs from the ILIAS standard.
<?xml version = "1.0" encoding = "UTF-8"?>
<service xmlns="http://www.w3.org" version="$Id$"
   id="trac">
   ...
   <crons>
      <cron id="lp_object_statistics" class="ilLPCronObjectStatistics" />
   </crons>
</service>

There are 3 basic concepts: cron-job, schedule and cron-result. Using them as intended should make testing and/or debugging of cron-jobs far easier than before.

ilCronJob

This base class must be extended by every cron-job. Besides the methods mentioned below isDue() is noteworthy as it is the central point by which the system decides if a cron-job is to be run or not. The "condition" of the existing CronCheck would have to be implemented here.

Several abstract methods have to be implemented to make a new cron-job usable:

  • getId(): returns the Id as defined in the module.xml or service.xml
  • hasAutoActivation(): is the cron-job active after "installation" or should it be activated manually?
  • hasFlexibleSchedule(): can the schedule be edited by an adminstrator or is it static?
  • getDefaultScheduleType(): see Schedule
  • getDefaultScheduleValue(): see Schedule
  • run(): process the cron-job

ilCronJobResult

The class ilCronJobResult determines the status of a current cron job.

The status are: * STATUS_INVALID_CONFIGURATION This status will indicate the current configuration for this cron job is not correct and MUST be adjusted. * STATUS_NO_ACTION This status indicates that the cron job did not perform any action. Possible reasons to set this action: * A cron job responsible to sent emails didn't sent emails at all. * A cron job responsible for deleting orphaned objects did not find any object to delete. * A lucene cron job decided that the index does not require an update. * STATUS_OK This status indicates that the cron job has been successfully finished. * STATUS_CRASHED A critical failure has occurred during the execution of the cron job, which led to an critical error. The cron job needs to be restarted by the administrator. * STATUS_RESET This status indicates that cron job has been rested. * STATUS_FAIL This status indicates that an non-critical error appeared in the execution of the cron job.

Every run()-Method of a cron job MUST return an instance of ilCronJobResult and MUST set status before returned by a method.

public function run(): ilCronJobResult
{
  $result = new ilCronJobResult();

  try {
    $procedure->execute();
    $result->setStatus(ilCronJobResult::STATUS_OK);
  } catch (Exception $exception) {
    $result->setStatus(ilCronJobResult::STATUS_FAIL);
    $result->setMessage($exception->getMessage());
  }

  return $result;
}

A message SHOULD be added additionally. If given, this message will be displayed in the cron job overview table.

Schedule

As the cron-tab (for 4.4+) should be configured in a way that it runs every few minutes the schedule is crucial to minimize system load by only running cron-jobs when really needed.

The are several schedule types available. The most basic are daily, weekly, monthly, quarterly and yearly. Furthermore a schedule can be given in minutes, hours and days. If the cron-job is set to a flexible schedule it can be configured by administrators. In any case a default schedule type and value (see above) has to be given to give users a clue when a job is due.

Use the following enum cases to implement your desired schedule:

\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_DAILY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_MINUTES;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_HOURS;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_DAYS;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_WEEKLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_MONTHLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_QUARTERLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_YEARLY;

Settings

A cron-job may define its own custom settings. The following methods can be used:

  • hasCustomSettings()
  • addCustomSettingsToForm()
  • saveCustomSettings()
  • addExternalSettingsToForm()
    • This method can be used to integrate cron-job specific settings into 1 or more administration settings forms.
    • The concept of "External Settings" is currently undocumented.

Customizing

  • getTitle()
  • getDescription()
  • activationWasToggled()
    • This hook will be called each time the activation status of the cron-job is changed. This way the cron-job can notify other classes about this.

Misc

The isDue() method decides if a cron-job is run or not. As a default it checks the schedule or runs anyways if triggered manually by an administrator. The specific cron-job is free to decide by any means necessary if it is currently active or not. A flexible schedule should of course mind the current settings.

A cron-job is set to crashed if it has been running for 3 hours straight and has not "pinged" once. The $DIC->cron()->manager()->ping() method will reset this counter thus enabling the cron-job to run as long as needed. So if you know that your cron-job might take ages add a ping() call at some sensible point in your run() method.

A cron-job is locked when running. The cron manager will take care of this and prevent concurrent runs. So as mentioned above the cron-tab can safely be set to every few minutes.

Cron Job Execution

In order to execute the cron job manager, the following command MUST be used:

/usr/bin/php [PATH_TO_ILIAS]/cron/cron.php run-jobs <user> <client_id> run-jobs

The <user> MUST be a valid (but arbitrary) user account of the ILIAS installation. The <client_id> MUST be the client id of the ILIAS installation.

Permission Context

Implementations of ilCronJob MUST NOT rely on specific permissions (e.g. RBAC). Generally said, there MUST NOT be any expectations regarding given permissions at all in the context of a cron job. Please keep this in mind when you structure your code layers.

 

Export

Export/Import of Components, esp. Repository Resources

Export

Export of a repository resource in this context means: content export without user related data. There are other exports like exports of members data in courses or groups. This how-to focuses on the export of a repository resource as a whole.

ILIAS provides support for a standard way of representing ILIAS export screens and for a standardized modularized export process.

Export User Interface: Class ilExportGUI

The export tab should be placed accoring to the tabs guidelines. The export screen should be implemented by using class ilExportGUI which is located in components/ILIAS/Export/classes. The basic lines of code needed are:

// the code is placed in the ilObj...GUI class of the repository resource
...
* @ilCtrl_Calls ilObj...GUI: ilExportGUI
...
// in execute command:
...
$exp_gui = new ilExportGUI($this); // $this is the ilObj...GUI class of the resource
$ret = $this->ctrl->forwardCommand($exp_gui);
...
// in set/get tabs:
if ($ilAccess->checkAccess("write", "", $this->object->getRefId()))
{
    $tabs_gui->addTab("export",
        $lng->txt("export"),
        $this->ctrl->getLinkTargetByClass("ilexportgui", ""));
}
...

The simple example above can be found in ILIAS/Exercise/classes/class.ilObjExerciseGUI.php. A more complex usage can be found in learning modules, class ILIAS/LearningModule/classes/class.ilObjContentObjectGUI.php.

Class ilExporter

For the XML export the export user interface class calls a generic export method ilExport->exportObject (ILIAS/Export). To use this generic export, you need to implement an il\<Module>Exporter class that is derived from ilXmlExporter.

The method exportObject of ilExport triggers a modularized export process that is not limited to repository resources. It can be used by any component of ILIAS for different sets of entities. E.g. the service MetaData implements a class ilMetaDataExporter and the module MediaPool implements a class ilMediaPoolExporter.

An il\<Component>Exporter class must provide:

  • a getXmlRepresentation() method
  • a getValidSchemaVersions() method
  • a getXmlExportHeadDependencies() method, if there are any dependencies to other components, that should be exported before the data of the current component
  • a getXmlExportTailDependencies() method, if there are any dependencies to other components, that should be exported after the data of the current component

Additionally the class may implement an init() method that is called at the beginning of the procedure.

getXmlRepresentation()

The method getXmlRepresentation() must return the XML for a given entity, target release and id.

  • entity: This is a string that represents the entity that should be exported, e.g. the object type like "lm", "file", "frm" or another identifier that is recognised by the component. E.g. the metadata component uses only one entity named "md".
  • target_release: A string like "4.1.0" that identifies the target release for the export. This allows to implement export routines for older versions than the current one.
  • id: The ID is the ID of the concrete entity. If an entity ID consists of multiple parts, they should be concatenated in one string using the ":" separator between each part.

The following example shows the implementation of the getXmlRepresentation method for meta data:

public function getXmlRepresentation(string $a_entity, string $a_target_release, string $a_id) : string
{
    $id = explode(":", $a_id);
    $mdxml = new ilMD2XML($id[0], $id[1], $id[2]);
    $mdxml->setExportMode();
    $mdxml->startExport();

    return $mdxml->getXml();
}

getXmlExportHeadDependencies() and getXmlExportTailDependencies()

These method must return the dependencies for a given entity and target release and multiple ids.

An example for a dependency is "every media object has metadata". In this case the media object component is responsible for providing the XML for the media object, but not for the metadata. The latter is defined as a "tail dependency": The metadata should be put into the export package after the media object has been exported.

To decide whether something is a head or a tail dependency (should be imported before or after the current component), it is important to consider how the package will be imported and how relations between the data are resolved.

The import processes the data in the same sequence as the export did. The import process provides an "ID mapping" feature to get new IDs (the ones created when imported) for old ones (the ones in the package).

If data of component (a) needs the new IDs of component (b) when being imported, (b) should be exported before (a) and if (b) depends on (a), (a) should be a tail dependency in the ilExporter clas of component (b).

Example: The metadata IDs of a media object are derived from the ID of a media object. If a media object has ID 5, the corresponding metadata ID will be "0:5:mob". This means, the media object should be imported before the metadata and the metadata should be a tail dependency in the media object export.

public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, string $a_ids) : string
{
    $md_ids = array();
    foreach ($a_ids as $mob_id)
    {
        $md_ids[] = "0:".$mob_id.":mob";
    }
    return array (
        array(
            "component" => "components/ILIAS/MetaData",
            "entity" => "md",
            "ids" => $md_ids)
        );
}

getValidSchemaVersions()

The export process relies on schema definitions (xsd files) that define the XML returned by the ilExporter classes. The getValidSchemaVersions() method must return all schema versions that the component can currently export to. ILIAS chooses the first one, that has min/max constraints which fit to the target release. Please put the newest schemas on top.

function getValidSchemaVersions(string $a_entity) : array
{
    return array (
        "4.1.0" => array(
            "namespace" => "http://www.ilias.de/components/ILIAS/MediaObjects/mob/4_1",
            "xsd_file" => "ilias_mob_4_1.xsd",
            "uses_dataset" => true,
            "min" => "4.1.0",
            "max" => "")
    );
}

The xsd files are located in the xml folder of ILIAS. The namespace should always have the format https://www.ilias.de/<Services|Modules>/<Component>/<entity>/<release number in the format x_x>.

Output of the Export Process

The standard export process creates a file named <timestamp>__<installation id>__<entity>_<id>.zip. This zip file contains a manifest.xml file on the top level, that includes basic information and references to export.xml files that have been created by the components involved during the export process.

<Manifest MainEntity="mep" Title="My Pool" TargetRelease="4.1.0"
InstallationId="4411" InstallationUrl="http://scott.local/ilias">
    <ExportFile Component="components/ILIAS/MediaObjects" Path="components/ILIAS/MediaObjects/set_1/export.xml"/>
    <ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_1/export.xml"/>
    <ExportFile Component="components/ILIAS/MediaPool" Path="components/ILIAS/MediaPool/set_1/export.xml"/>
    <ExportFile Component="components/ILIAS/MediaObjects" Path="components/ILIAS/MediaObjects/set_2/export.xml"/>
    <ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_2/export.xml"/>
    <ExportFile Component="components/ILIAS/File" Path="components/ILIAS/File/set_1/export.xml"/>
    <ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_3/export.xml"/>
    <ExportFile Component="components/ILIAS/COPage" Path="components/ILIAS/COPage/set_1/export.xml"/>
</Manifest>

The example above shows the manifest file for a media pool which lists all components that are involved in the export of the media pool and their export files.

Import

ILIAS also provides a standardized import process for packages that have been created with the standard export process. The process for importing a repository resource is started with a call of ilImport->importObject (ILIAS/Export). All involved components must provide a class il<Component>Importer that handles the import of the corresponding data.

Class ilImporter

This class must be derived from ilXmlImporter (ILIAS/Export). The class must provide:

  • a method importXmlRepresentation() that imports the XML of the export.

Additionally the class may implement an init() method that is called at the beginning of the procedure.

function importXmlRepresentation(string $a_entity, string $a_id, string $a_xml, ilImportMapping $a_mapping) : void
{
    $new_id = $a_mapping->getMapping("components/ILIAS/MetaData", "md", $a_id);
    if ($new_id != "")
    {
        $id = explode(":", $new_id);
        $xml_copier = new ilMDXMLCopier($a_xml, $id[0], $id[1], $id[2]);
        $xml_copier->startParsing();
    }
}

The example above imports the XML data that has been created by the corresponding il<Component>Exporter class, in this case ilMetaDataExporter. The ID provided is the ID of the export file. In this case the new id depends on the object that depends on the meta data, e.g. a media object. The media object has been imported before in this case and provided mapping information for the metadata via the mapping object that is passed to importXmlRepresentation. The mapping object allows to add (set) old-to-new-ID mappings and to lookup them up (get).

// code in ILIAS/MediaObjects that creates new media objects from
// import xml and adds mapping for metadata
$newObj = new ilObjMediaObject();
...
$newObj->create();
...
$a_mapping->addMapping("components/ILIAS/MetaData", "md",
    "0:".$old_id.":mob", "0:".$newObj->getId().":mob");

Validation

The import validation is only enabled if a schema file is available. It is important that the schema file is located in the directory 'xml/SchemaValidation' and that the naming convention is followed.

Schema File Naming Convention

Schema files have to follow the naming convention:

ilias_{type_string}_{version_string}.xsd

'type_string' can either be or _. With 'type' beeing the component id found in the components corresponding module.xml or service.xml. The type of export xml files is set in the functions getXmlExportTailDependencies() and getXmlRepresentation(). 'type' corresponds to the attribute entity in the xml file. For example, the component id/type/entity value of Course is 'crs'.

'version_string' follows the pattern: _. The 'version_string' is defined in getValidSchemaVersions().

To determine the matching schema file for a given xml-export file, the value of 'type_string' is compared with the value of the attribute 'entity' of the 'exp:Export'-node and the value of 'version_string' is compared with the value of the attribute 'SchemaVersion' of the 'exp:Export'-Node.

If the xml-export file contains a dataset, the 'entity' attribute of the 'ds:Rec'-nodes is used instead of the 'entity' attribtue of the 'exp:Export'-node.

If the Version numbers do not match, the schema file with the highest version number is used.

For example take a look at 'ilias_crs_objectives_9_0.xsd'. Here 'type_string' is 'crs_objectives' with type 'crs' and subtype 'objectives'. 'version_string' is '9_0' with 'major_version_number' 9 and a 'minor_version_number' 0.

Updating Schema File Versions

During development xml file specifications may change, wich in consequence requires a new xsd. The first step is to create a new xsd and add it to the 'xml/SchemaValidation'-folder. After that an entry with the correct version string needs to be added to the components ilExporter getValidSchemaVersions()-function. If the import of older xml files should no longer be possible, the old xsd-file needs to be removed from the 'xml/SchemaValdiation'-folder and the components ilExporter getValidSchemaVersions()-function should be adjusted accoirdingly.

Enable Import Validation

Add the schema file to the 'xml/SchemaValidation'-folder.

Disable Import Validation

Remove the schema from in the 'xml/SchemaValidation'-folder.

Validation Code Examples

Validate Xml File:

// Get the xml SplFileInfo
$xml_file_spl = new SplFileInfo('path to my xml file')

// Get the xsd SplFileInfo
$xsd_file_spl = new SplFileInfo('path to my xsd file')

// Initialize a xml/xsd file handler
$import = new \ILIAS\Export\ImportHandler\Factory();
$xml_file_handler = $import->file()->xml()->handler()->withFileInfo($xml_file_spl);
$xsd_file_handler = $import->file()->xsd()->hanlder()->withFileInfo($xsd_file_spl);

/** @var \ILIAS\Export\ImportStatus\ilCollection $validation_results */
// Validate
$validation_results = $import->file()->validation()->handler()->validateXMLFile(
    $xml_file_handler,
    $xsd_file_handler
);

// Check if an import failure occured
if ($validation_results->hasStatusType(\ILIAS\Export\ImportStatus\StatusType::FAILED)) {
    // Do something on failure
}

Validate Xml at Xml Node:

// Get the xml SplFileInfo
$xml_file_spl = new SplFileInfo('path to my xml file')

// Get the xsd SplFileInfo
$xsd_file_spl = new SplFileInfo('path to my xsd file')

// Initialize a xml/xsd file handler
$import = new \ILIAS\Export\ImportHandler\Factory();
$xml_file_handler = $import->file()->xml()->handler()->withFileInfo($xml_file_spl);
$xsd_file_handler = $import->file()->xsd()->handler()->withFileInfo($xsd_file_spl);

// Build xPath to xml node
// $path->toString() = '/RootElement/namespace:TargetElement'
/** @var \ILIAS\Export\ImportHandler\Path\Handler $path */
$path = $import->path()->handler()
    ->withStartAtRoot(true)
    ->withNode($import->path()->node()->simple()->withName('RootElement'))
    ->withNode($import->path()->node()->simple()->withName('namespace:TargetElement'));

// Because the path contains the namespace 'namespace' we have to add the namespace
// info to the xml file handler
$xml_file_handler = $xml_file_handler->withAdditionalNamespace(
    $import->file()->namespace()->handler()
        ->withNamespace('http://www.example.com/Dummy1/Dummy2/namespace/4_2')
        ->withPrefix('namespace')
)

/** @var \ILIAS\Export\ImportStatus\ilCollection $validation_results */
// Validate
$validation_results = $import->file()->validation()->handler()->validateXMLAtPath(
    $xml_file_handler,
    $xsd_file_handler,
    $path
);

// Check if an import failure occured
if ($validation_results->hasStatusType(\ILIAS\Export\ImportStatus\StatusType::FAILED)) {
    // Do something on failure
}

Using Dataset Classes for Import/Export

It is up to the component, how the XML for the export is created and imported. The component can, but does not need to, make use of dataset classes that create XML in a generic way without the need to use the ilXmlWriter class for export or individual sax import parsers classes.

 

Help

Online Help Support

The online help is currently supported for the German language only.

Within the online help learning modules, help texts are assigned to screens of the ILIAS application. Screens are identified by screen IDs consisting of three parts Component/Screen/SubScreen. To set the screen ID for your component, use the global $DIC->['ilHelp'] object. It provides the following methods:

  • setScreenIdComponent($a_comp);
  • setScreenId($a_screen);
  • setSubScreenId($a_subscreen_id);

These methods should be called in GUI classes of your component. Best practice is to do this only if the corresponding class implements the current screen (handles the current command). A good place is the method that is also responsible for setting the tabs/subtabs.

At minimum the setScreenIdComponent method must be called. If no screen and subcreen IDs are provided, but tabs and or subtags are displayed on the screen. ILIAS will use the IDs of the tabs/subtabs automatically to extend the overall screen ID.

 

Language

 

 

Logging

Logging Service

Starting with release 5.1 a new logging service based on Monolog is available.

The service provides support for different log levels per component.

Activate Logging for Components

To use different log levels for your component, you have to enable "logging" in your module.xml or service.xml.

<?xml version = "1.0" encoding = "UTF-8"?>
<module xmlns="http://www.w3.org" version="$Id$" id="grp">
...
<!-- add a line "logging" the component definition file -->
    <logging />
</service>

Call composer du to trigger the reading of the XML files.

Different log levels for components can be defined in "ILIAS -> Administration -> Logging -> Components". If no component specific log level is given, the the global log level is used.

Definition of Log Levels

ILIAS (monolog) support the following log levels defines in RFC 5424:

  • DEBUG: Detailed debug information
  • INFO: Interesting event. E.g user logs in
  • NOTICE: Normal but significant events
  • WARNING: Exceptional occurences that are no errors. E.g calls of deprecated methods
  • ERROR: Runtime errors that do not require immediate action
  • CRITICAL: Critical conditions - e.g. a module service is unasable due to missing librarys.
  • ALERT: Immediate action is required. E.g. no database connection
  • EMERGENCY: The system is unusable.

Using the Logging Service

An instance of the logging service is available via $DIC->logger(). You should reveice the logger for your component by calling a method with your component id on this object.


// Get component logger $grp_logger = $DIC->logger->grp(); // write a message with info log level $grp_logger->info('info message');

Using Placeholders

The ilLogger exposes the placeholder feature given by the monolog bundle, which implements a PSR-3 compliant logger interface.

Placeholders should be used to allow escaping of user input just as $database->quote(...) is used to escape user input in SQL queries.

Example usage

$logger->debug('Lorem ipsum  dolor .', [
    'foo' => 'Lorem',
    'bar' => 'ipsum',
]);

Further reading

Please read the PSR-3 Specification for further information.

 

Mail

Manual Mail Templates

The concept of 'Manual Mail Templates' is best described as a feature which enables components to provide text templates for a 'User-to-User Email' in a specific context.

Often tutors / admins send the emails with the same purpose and texts to course members, e.g. to ask them if they have problems with the course because they have not used the course yet.

Context Registration

A module or service MAY announce its email template contexts to the system by adding them to their respective module.xml or service.xml. The template context id has to be globally unique. An optional path can be added if the module/service directory layout differs from the ILIAS standard, where most files are located in a ./classes directory.

<?xml version = "1.0" encoding = "UTF-8"?>
<module xmlns="http://www.w3.org" version="$Id$" id="crs">
    ...
    <mailtemplates>
        <context id="crs_context_manual" class="ilCourseMailTemplateContext" />
    </mailtemplates>
</module>

If registered once, every email template context class defined in a module.xml or service.xml has to extend the base class \ilMailTemplateContext. All abstract methods MUST be implemented to make a template context usable.

  • getId(): string
  • getTitle(): string
  • getDescription(): string
  • getSpecificPlaceholders(): array
  • resolveSpecificPlaceholder(string $placeholderId, array $contextParameters, \ilObjUser $recipient = null, $htmlMarkup = false): string

A collection of context specific placeholders can be returned by a simple array definition. The key of each element should be a unique placeholder id. Each placeholder contains (beside its id) a placeholder string and a label which is used in the user interfaced.

return [
    'crs_title' => [
        'placeholder' => 'CRS_TITLE',
        'label' => $lng->txt('crs_title')
    ],
    'crs_link' => [
        'placeholder' => 'CRS_LINK',
        'label' => $lng->txt('crs_mail_permanent_link')
    ]
];

Supposing the context registration succeeded and you properly derived a context PHP class providing all necessary data and placeholders, you are now able to use your registered context in your component and build hyperlinks to the mail system, transporting your context information.

Context Usage Example

Given you created a context named crs_context_tutor_manual ...

global $DIC;

class ilCourseMailTemplateTutorContext extends \ilMailTemplateContext
{
    // [...]
    const ID = 'crs_context_tutor_manual';
    // [...]
}

... you can provide a hyperlink to the mail system (or more precisely to \ilMailFormGUI) as follows:

$DIC->ctrl()->redirectToUrl(
    \ilMailFormCall::getRedirectTarget(
        $this, // The referring ILIAS controller aka. GUI when redirecting back to the referrer
        'participants', // The desired command aka. the method to be called when redirecting back to the referrer
        [], // Key/Value array for parameters important for the ilCtrl/GUI context when when redirecting back to the referrer, e.g. a ref_id
        [
           'type' => 'new', // Could also be 'reply' with an additional 'mail_id' paremter provided here
        ],
        [
            \ilMailFormCall::CONTEXT_KEY => \ilCourseMailTemplateTutorContext::ID, // don't forget this!
            'ref_id' => $courseRefId,
            'ts'     => time(),
            // further parameters which will be later automatically passed to your context class 
        ]
    )
);

Parameters required by your mail context class MUST be provided as a key/value pair array in the fifth parameter of \ilMailFormCall::getRedirectTarget. These parameters will be passed back to your context class when the mail system uses \ilMailTemplateContext::resolveSpecificPlaceholder(...) as callback when an email is actually sent and included placeholders should be replaced. You also MUST add a key \ilMailFormCall::CONTEXT_KEY with your context id as value to this array.

 

Skill

Skill Service

This how-to describes how components can and should use the Skill Service. The Skill Service implements what is called in ILIAS the Competence Management. The different terms have historical reasons, as the feature has been called Skill Management in the beginning.

The Competence Tree

Competences are organized in a hierarchical structure. Nodes that contain subnodes are called Competence Category, leaf nodes are Competences. These competences define an ordered list of competence levels.

This simple structure becomes complex with the introduction of competence templates. Competence templates are reusable subtrees that can be referenced within the main competence structure by Competence Template References.

The competence templates can either only consist of one basic competence template (without and subnodes) or of a competence template category including subnodes.

Node Internal Type Purpose Part of the Hierarchy
Root Node of Competence Tree skrt Root of the Competence Hierarchy
Competence Node skll Defines Competence Levels Main Part
Competence Category Node scat Can contain competence and competence category nodes Main Part
Competence Template Reference Node sktr References a competence template or a competence template category Main Part
Competence Template Node sktp Defines Competence Levels Template Part
Competence Template Category Node sctp Can contain competence templates or competence template categories Template Part

Identifying a Competence

We now focus on the leaf nodes in the competence tree that define the competence levels (either type "skll" or type "sktp").

If no competence templates would be used, a competence could simply be identified by its node ID. But since competence templates can be reused multiple times we need a second node ID, the ID of the competence reference to identify a competence.

Structure of an ID for a competence: <skill_id>:<tref_id>

If the skill_id is the ID of a simple competence node (type "skll"), the tref_id must be 0. If the skill_id is the ID of a competence template node (type "sktp") the tref_id must be the ID of a reference node (type "sktr"). A reference node refers to a template identified by the root node of the template (either type "sktp" or "sctp"). The node with the ID skill_id must be within the subtree of this template.

The most important to keep in mind is that a competence is identified by the two parts <skill_id>:<tref_id>.

 

Tabs

Using Tabs

Since 3.10 a simpler handling for tabbed menues has been introduced. One methode usually defines the main tabs in a GUI class, many times called setTabs(). The methods that execute a certain command activate the corresponding tab. Tabs are identified by ids. A global $ilTabs instance is used to add/activate tabs and subtabs.


function __construct(ilTabsGUI $tabs) { $this->tabs = $tabs; // alternatively throug $DIC->tabs(); } // view command function view() { [...] $this->tabs->activateTab("id_view"); [...] } // edit command function edit() { [...] $this->tabs->activateTab("id_edit"); [...] } // setting main tabs function setTabs() { global $ilAccess, $lng, $ilTabs, $ilCtrl; if ($this->access->checkAccess("read", "", $this->object->getRefId())) { // add main tab, id $this->tabs->addTab("id_view", $this->lng->txt("view"), $this->ctrl->getLinkTarget($this, "view")); } if ($this->access->checkAccess("write", "", $this->object->getRefId())) { $this->tabs->addTab("id_edit", $this->lng->txt("edit"), $this->ctrl->getLinkTarget($this, "edit")); } }

\ Common methods that should be used:

  • Add a tab: $tabs->addTab($a_id, $a_text, $a_link, $a_frame = "");

  • Activate a tab: $tabs->activateTab($a_id);

  • Add a subtab: $tabs->addSubTab($a_id, $a_text, $a_link, $a_frame = "");

  • Activate a subtab: $tabs->activateSubTab($a_id);

  • Clear all tabs: $tabs->clearTargets();

  • Clear subtabs: $tabs->clearSubTabs();

\ Other methods that are rarely used:

  • Remove a tab: $tabs->removeTab($a_id);

  • Replace a tab: $tabs->replaceTab($a_old_id,$a_new_id,$a_text,$a_link,$a_frame = '');

\ For help with the correct ordering of tabs please read the tabs guideline.

 

Taxonomy

Using Taxonomies

Taxonomy Settings in Repository Objects (ILIAS 9)

The taxonomy service is available via the DIC.

$taxonomy_service = $DIC->taxonomy();

Activation Setting

The general taxonomy service should be activated as "Additional Feature" in the main settings screen by using ilObjectServiceSettingsGUI with the ilObjectServiceSettingsGUI::TAXONOMIES flag.

Taxonomy Settings Subtab

You should add the taxonomy settings as a subtab under your main settings tab. This can be done by passing the object ID of your repository object to addSettingsSubTab. Note that the subtab will only appear if the general setting is activated for your object.

$taxonomy_service->gui()->addSettingsSubTab($obj_id);

Forwarding to the Taxonomy Settings

You need to forward to the ilTaxonomySettingsGUI class.

ilCtrl Declaration:

/**
 * @ilCtrl_Calls ...: ilTaxonomySettingsGUI
 */

Forwarding in executeCommand:

...
    case strtolower(ilTaxonomySettingsGUI::class):
        $tax_gui = $taxonomy_service->gui()->getSettingsGUI(
            $obj_id
        );
        $this->ctrl->forwardCommand($tax_gui);
        break;
...

The getSettingsGUI method provides four parameters, only the first one is mandatory:

  • (int) Object ID of your repository object
  • (string) An information text that is displayed on top of the settings screen
  • (bool) If true, multiple taxonomies can be created for your repository object
  • (\ILIAS\Taxonomy\Settings\ModifierGUIInterface) An object that implements the ModifierGUIInterface interface.

Settings Modifier

The ModifierGUIInterface interface allows to add additional properties to the taxonomy items in the taxonomy list and to add additional actions for each taxonomy in the list. This way your context may introduce separate settings for each taxonomy, e.g. the Category module allows to activate presentations of taxonomies as side blocks in the presentation.

Taxonomies as Table Filter

The input class is called ilTaxSelectInputGUI. Since this class makes subsequent requests that must be routed to an instance of the class, your table must be included in the ilCtrl control flow and forward commands to the form of the filter.

Your TableGUI class must...

  • include a @ilCtrl_Calls comment for ilFormPropertyDispatchGUI and
  • add a ilTaXSelectInputGUI filter item in initFilter.
/**
 * ...
 * @ilCtrl_Calls ilPresentationListTableGUI: ilFormPropertyDispatchGUI
 */
class ilPresentationListTableGUI extends ilTable2GUI
{   
    ...

    /**
     * Init filter
     */
    function initFilter()
    {
        ...
        include_once("./components/ILIAS/Taxonomy/classes/class.ilTaxSelectInputGUI.php");
        $tax = new ilTaxSelectInputGUI($this->tax_id, "tax_node", true);
        $this->addFilterItem($tax);
        $tax->readFromSession();
        $this->filter["tax_node"] = $tax->getValue();
        ...
    }
}

The GUI class that outputs the table must...

  • include a @ilCtrl_Calls comment for your TableGUI class,
  • forward to your TableGUI class via executeCommand and
  • get the HTML of your TableGUI class by using $ilCtrl->getHTML($table);, not $table->getHTML();.
/**
 * ...
 * @ilCtrl_Calls ilGlossaryPresentationGUI: ilPresentationListTableGUI
 */
class ilGlossaryPresentationGUI
{
    ...
    /**
     * execute command
     */
    function executeCommand()
    {
        ...
        $next_class = $this->ctrl->getNextClass($this);
        ...
        switch($next_class)
        {

            case "ilpresentationlisttablegui":
                $prtab = $this->getPresentationTable();
                $ilCtrl->forwardCommand($prtab);
                break;


            default:
                ...
                break;
        }
        $this->tpl->show();
    }

    function getPresentationTable()
    {
        include_once("./components/ILIAS/Glossary/classes/class.ilPresentationListTableGUI.php");
        $table = new ilPresentationListTableGUI($this, "listTerms", $this->glossary,
            $this->offlineMode(), $this->tax_node, $this->glossary->getTaxonomyId());
        return $table;
    }

    function listTerms()
    {
        ...
        $table = $this->getPresentationTable();
        $tpl->setContent($ilCtrl->getHTML($table));
    }
}

 

Tracking

Learning Progress

Note: This documentation may not be complete, but the points documented should (still) be correct. Reports of missing or wrong information using the ILIAS issue tracker or contributions via Pull Request are greatly appreciated.

Core concepts

The learning progress service in ILIAS consists of 3 core concepts:

  • Change Event
  • (Learning Progress) Status
  • (Learning Progress) Marks

The other components like (object) statistics and session statistics which can be found in Administration > Statistics and Learning Progress are not part of this how-to.

Change Event

Not to be confused with ILIAS event handling, the change event service keeps track of user activity in ILIAS repository objects. There are read and write events. The learning progress only utilizes read events which are recorded / updated for each "click" inside a repository object. Write events are mostly restricted to changes in object settings or new repository objects and are the basis for the "changed inside" messages in the repository object lists.

Read events mostly consist of 2 figures which are tracked for each repository object and user:

  • read count (or "requests")
  • spent seconds (or "time spent")

For every "click" or request ILIAS calculates the time which has passed since the last request and depending on the administration setting "Max. Time Between Requests" will - add the time to the "spent seconds" figure of the current object if the time passed is below the threshold - increment the "read count" figure of the current object and ignore the time passed if it is above the threshold Both figures will also be added to all (current) parent objects.

DB: "change_event"

LP status

The learning progress status is designed to make the different repository object types comparable regarding user activity and its result. There are several different ways a LP status is calculated - called LP modes - and those can be configured for most object types which support the learning progress feature.

A learning progress status has 4 possible values:

  • "not attempted": the user has no recorded activity
  • "in progress": the user has recorded activity but no result yet
  • "completed": the user has completed the object
  • "failed": the user has failed the object

Please keep in mind that "object" is not limited to repository object here but can also mean SCO, learning module chapter, learning objective and so on.

Each repository object type supports different modes of learning progress, e.g.

  • Manual by Tutor
  • Automatic by Collection of Objects
  • Manual by Learner
  • Test finished and so on.

Currently the learning progress does not support any notion of a final status. At anytime a user LP status may change for an object. On changing the learning progress mode for a repository object - which is always possible - all LP status for existing users are re-calculated.

The LP status calculation (including the optional percentage) has to be triggered by a repository object each time "something" changed that might result in a LP status change. That "something" depends on the current LP mode for that object. The most simple example would be the 1st read event or 1st click of a user inside an object which (most of the time) results in a status change from "not attempted" to "in progress".

See:

  • ilLearningProgress::_tracProgress()
  • ilLPStatusWrapper::_updateStatus()

Do NOT use ilLPStatusWrapper::_refreshStatus(), which will re-calculate the LP status for the complete object. Please refer to the ilObjectLP::resetLPDataFor*-methods on how to remove LP data properly. Each repository object type that supports learning progress has its own "connector" class which extends ilObjectLP.

Hint: there is a LPStatus*-class for each LP mode, do not get confused by this.

DB: "ut_lp_marks" (status, status_changed, status_dirty, percentage)

Collections

A collection consists of 1-n sub-objects which can be repository objects, SCOs, learning modules or learning objectives. There is no way to discern this in the DB, it depends on the LP mode of the parent object. The LP status of a collection is determined by the status of its sub-objects (and their optional groupings). Every time a LP status changes for an object every parent collection is updated accordingly. This can lead to chains of updates for nested collections.

DB: "ut_lp_collections"

LP marks

This directly corresponds to the "edit"-form in the LP for single users. It is mostly used for "Manual by Tutor"-mode where the "completed"-flag translates to LP status "completed".

DB: "ut_lp_marks" (completed, mark, u_comment)

Misc

The complex LP DB queries can currently all be found in ilTrQuery. This might change.

Do not call any ilLPStatus*-class directly, use ilLPStatusWrapper.

The LP status is a mixture of progress and success information. Those 2 concepts are kept separate in SCORM. We are aware of this, but in ILIAS the focus for the learning progress design is to keep the LP status consistent and comparable for all object types.

Figures in LP statistics

  • Access/Time Spent
    • For each "click" ILIAS measures the time which passed since the last "click". if it is below the threshold (administration > lp > "max. time between requests") it will be counted as the same request and the time difference will be added to "Time spent" (for the current object). If is bigger than the threshold the Access Number is incremented and the time difference is ignored.
    • Things are completely different for SCORM modules though, as the player is more or less an external black box (which does the access/time spent handling by itself).
    • Please keep in mind that access number and time spent of sub-items, e.g. objects in courses, will be added to their parent (in the LP statistics). Furthermore if you move sub-items to a different parent, the numbers might not add up at all.
  • Percentage
    • This is decided by the specific test LP mode. "Test passed" seems to use points as basis (not number of questions).
  • Last Status Change
    • "last access" is the last "click"/action in an object, "last status change" means the point in time when the LP status changed for the last time, e.g. from "in progress" to "completed".
  • Total Time Online
    • This is the time spent logged into ILIAS regardless of object or context. The access/time spent logic also applies here (see above). We might discuss the presentation of this in the near future, currently we would favour to include it in the user management and remove it from the LP statistics.
  • Last Login
    • This is the datetime of the last login whereas "last access" is updated on each "click" (and for each object).
  • Working Time
    • This is test-specific and is not supplied by the learning progress.

Permissions

  • read_learning_progress: read learning progress of other users
    • This gives access to the LP data of others users in the LP statistics in the repository If a user can see his/her own LP status is determined by the administration setting "Accesible Personal Learning Progress".
  • edit_learning_progress
    • This allows to edit the LP settings of a repository object and edit the LP data of object "members": comment, mark, completed.
  • "See learning progress overview of other users" (Administration > LP)
    • This gives access to the tab "Users" in Personal Desktop > LP. Due to performance reasons we cannot use object permissions to determine the access.

 

User

User Data

Access Data of the User Currently Logged in

An instance of class ilObjUser for the user currently logged in ILIAS is available through the DI-Container. You can use the public methods of this class to access the data of the current user (see components/ILIAS/User/classes/class.ilObjUser.php):

function foo()
{
    global $ilUser;
    [...]
    $user_id = $ilUser->getId();
    $user_firstname = $ilUser->getFirstname();
    $user_lastname = $ilUser->getLastname();
    [...]
}

Standard User Name Presentation

The class ilUserUtil provides a static method called getNamePresentation(...) that should be used to display user first and last name whenever possible.

  • The login is always displayed as [login]
  • First and last name are only displayed if there is a public profile (this can be overridden by parameter $a_force_first_lastname)
  • Optionally the user image can be included
  • Optionally a link to the public profile of the user can be included
$this->tpl->setVariable("TXT_USER",
    ilUserUtil::getNamePresentation($user_id, true, true, $back_link));

 

Web Access Checker

Web Access Checker

What's new in ILIAS 5.1

  • Web Access Checker is enabled by default
  • Now all Files in the ./data directory will be checked
  • New Token-Based File delivery (much faster than before)

The new WebAccessChecker allows fast and secure delivery of files in the /data directory (see Feature-Wiki for performance comparison). As definied in the Data Directory Guideline, directories within the /sec-Folder are supported. Since there are folders outside the /sec-Folder which have to be secured, the new WebAccessChecker also secures those directories (e.g. lm_data, usr_images, mobs).

All requests to /data are now redirected to the WAC-Script per default, the .htaccess-File has a new entry:

RewriteRule ^data/.*/.*/.*$ wac.php [L]

The WAC delivers the file after the following decisions:

Web Access Checker decisions scheme

The most performant ways to deliver files are the file- or the folder-based tokens. These requests are already signed and and access-checked during the rendering of a page (e.g. display of poll-image, the user accesses a course and ILIAS renders a poll-image to the page. At this moment the access is already checked).

If there is no token or the token has become invalid, e previosly registred checking-instance has to decide whether the file can be delivered or not. if theres no checking instance, only files outside the /secore-Folder will bedelivered. if there's a checking instance and the instance declines delivery, access to the file is also denied.

Implementation

Signing Files

Developers can sign files and folders using the ilWACSignedPath-Class:

// Example in Poll:
$img = $a_poll->getImageFullPath();
$this->tpl->setVariable("URL_IMAGE", ilWACSignedPath::signFile($img));

// Example in SCORM-Module
ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory().'/manifest.xml');

Register Checking instance

When registering a checking instance, developers have to add a secured path to their service.xml or module.xml:

<web_access_checker>
<secure_path path="ilPoll" checking-class="ilObjPollAccess" in-sec-folder='1'/>
</web_access_checker>

This entry will be registred with the next structure reload (add one if you want to register a new secured path). It's allowed to have multiple checking-instances per module/service but they must have unique paths. After the reload all requests in ./data/my_client/sec/ilPoll/* will be checked by the class ilObjPollAccess. The method canBeDelivered() which is defined by the ilWACCheckingClass-interface receives the ilWACPath Object which proviedes several information about the requested file. Most Modules will look for the obj_id and check using ilAccess.

class ilObjPollAccess extends ilObjectAccess implements ilWACCheckingClass
{   
    // Other methods and checking functions of ilObjPollAccess

    /**
     * @param ilWACPath $ilWACPath
     *
     * @return bool
     */
    public function canBeDelivered(ilWACPath $ilWACPath) {
        global $ilAccess;
        preg_match("/\\/poll_([\\d]*)\\//uism", $ilWACPath->getPath(), $results);

        foreach (ilObject2::_getAllReferences($results[1]) as $ref_id) {
            if ($ilAccess->checkAccess('read', '', $ref_id)) {
                return true;
            }
        }

        return false;
    }
}

?>

Error Handling

Per default the Web Access Checker delivers on images and videos a error-placeholder when the user has no permission to access the file.

File Delivery

Delivering files is implemented using the ilFileDelivery-Service introduced in ILIAS 5. ilFileDelivery supports X-SendFile and introduces a updated ilMimeTypeUtil. Straming videos is now done chunked and allows (as previously in ilUtil) ranges. Methods in ilUtil are marked as deprecated.

// File-Delivery example
$ilFileDelivery = new ilFileDelivery('./components/ILIAS/WebAccessChecker/templates/images/access_denied.png', 'file_name.png');
$ilFileDelivery->setDisposition(ilFileDelivery::DISP_INLINE);
$ilFileDelivery->deliver();

If you want to use ilFileDeliver with X-SendFile please install and activate

sudo apt-get install libapache2-mod-xsendfile
sudo a2enmod xsendfile

In your Apache-Config or VHOST the "iliasdata"- and the "data"- directories must be unlocked, e.g.:

XSendFilePath /var/www
XSendFilePath /var/iliasdata

Additionally in the .htaccess the following rule activated the X-SendFile Module:


XSendFile On