Contributing

This section is for developers who want to contribute code to the tirreno project. If you only want to customize tirreno for your own use (custom rules, pattern lists), see the Risk rules & customization section above.

Notice: Submissions using generative AI will be rejected. Submissions from AI chatbots will result in the account being banned.

Source code

The source code is maintained at: https://github.com/tirrenotechnologies/tirreno

Before you start

Most issues in the tirreno issue tracker are ideas and bugs that the team would like to implement or solve. However, this is not always the case — the team may no longer be interested in some issues even though they remain open.

Before you spend time working on a bug or feature (and risk it not being merged), it is highly recommended that you first leave a comment on the issue explaining that you are interested in contributing. In your comment, also explain how you plan to solve the bug or implement the new feature, and ask for a quick validation of your approach.

This gives the tirreno team the opportunity to review your proposal, confirm whether they want to see it added, and provide early guidance. The team will reply in the issue, and once they confirm, you can confidently work towards opening a Pull Request.

If no existing issue matches your idea, create a new issue first and wait for team feedback before starting development.

Contributor license agreement (CLA)

Before your contributions can be accepted, you must sign the tirreno Contributor License Agreement (CLA). All contributed code is dual-licensed: AGPL-3.0 for open source use and a separate enterprise license for commercial use. Contact team@tirreno.com for the CLA document.

Git workflow

  1. Fork the repository on GitHub
  2. Clone your fork: git clone https://github.com/YOUR_USERNAME/tirreno.git
  3. Create a branch: git checkout -b feature/your-feature
  4. Make changes following coding standards
  5. Commit, push, and open a Pull Request

Local development setup

Prerequisites

Local setup

# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/tirreno.git
cd tirreno

# 2. Install dependencies
composer install

# 3. Create PostgreSQL database
createdb tirreno_dev

# 4. Configure database
# Edit config/ files with your database credentials

# 5. Run web installer
# Point Apache to project root, visit: http://localhost/install/

# 6. Delete install directory (important!)
rm -rf install/

# 7. Setup cron job
crontab -e
# Add: */10 * * * * /usr/bin/php /absolute/path/to/tirreno/index.php /cron

# 8. Create admin account at /signup/

Docker setup

One line:

curl -sL tirreno.com/t.yml | docker compose -f - up -d

Code quality tools

tirreno uses the following tools for code quality:

# PHP CodeSniffer - check style
./vendor/bin/phpcs --standard=phpcs.xml app/

# PHP CodeSniffer - auto-fix
./vendor/bin/phpcbf --standard=phpcs.xml app/

# PHPStan - static analysis
./vendor/bin/phpstan analyse

# ESLint - JavaScript
npx eslint ui/js/
npx eslint ui/js/ --fix

PHP coding standards

Class structure

Follow the tirreno Model pattern:

<?php
declare(strict_types=1);
namespace Tirreno\Models;

class Device extends \Tirreno\Models\BaseSql {
    protected $DB_TABLE_NAME = 'event_device';

    public function getFullDeviceInfoById(int $deviceId, int $apiKey): array {
        // ...
    }
}

Use fully-qualified class names and type declarations for all parameters and return types.

Naming conventions

ElementConventionExample
Classes/NamespacesPascalCaseDevice, BaseSql
Methods/VariablescamelCasegetDeviceInfo(), $apiKey
ConstantsUPPER_SNAKE_CASEDB_TABLE_NAME
Tables/Columnssnake_caseevent_device, api_key
Query params:snake_case:api_key, :device_id

Query string style

Use parentheses for multiline SQL queries:

$query = (
    'SELECT id, lang, created
    FROM event_device
    WHERE key = :api_key'
);

SQL security

Always use named PDO parameters:

// Good
$params = [':api_key' => $apiKey, ':device_id' => $subjectId];
$query = ('SELECT id FROM event_device WHERE id = :device_id AND key = :api_key');
$results = $this->execQuery($query, $params);

// Bad - never concatenate user input
$query = "SELECT * FROM event_device WHERE id = $deviceId";

Database best practices

Extend \Models\BaseSql, use execQuery(). Never raw PDO.

XSS prevention

Templates auto-escape with {{ @var }}. Use htmlspecialchars() at output time in PHP:

echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

Template syntax

tirreno uses the Fat-Free Framework's template engine with includes, variables, and inline PHP:

<include href="templates/parts/headerAdmin.html" />
<div id="wrap">
    <include href="templates/parts/panel/eventPanel.html" />
    <include href="templates/parts/panel/devicePanel.html" />
    <include href="templates/parts/leftMenu.html" />
    <div class="main">
        <include href="templates/parts/forms/globalSearchForm.html" />
        <include href="templates/parts/systemNotification.html" />
        <include href="templates/parts/notification.html" />

        {~
            $country = ['iso' => $IP['country_iso']];
            $subtitle = array();
            if(isset($IP['name']) && !empty($IP['name'])) {
                $subtitle[] = $IP['name'];
            }
            $subtitle = join(', ', $subtitle);
        ~}

        <include href="templates/parts/infoHeader.html" with="title={{@IP.ip}}, country={{@country}}, id={{@IP.id}}"/>
        <include href="templates/parts/widgets/ip.html" />
        <include href="templates/parts/tables/users.html" />
        <include href="templates/parts/tables/events.html" with="showChart=1"/>
    </div>
</div>
<include href="templates/parts/footerAdmin.html" />

Template conventions:

SyntaxPurposeExample
{{ @var }}Output escaped variable{{ @IP.ip }}
{{ @var | raw }}Output unescaped (careful!){{ @htmlContent | raw }}
{{ @arr.key }}Access array element{{ @IP.country_iso }}
{~ ... ~}Inline PHP code block{~ $x = 1 + 2; ~}
<include href="..." />Include template file<include href="templates/parts/header.html" />
<include ... with="..." />Include with parameters<include href="..." with="title={{@IP.ip}}, id={{@IP.id}}"/>
{<b> ... </b>}Template comment (not rendered){<b><include href="..." /></b>}

Template directory structure:

ui/templates/
├── layout.html             # Base layout
├── pages/                  # Page templates
│   ├── admin/              # Admin page templates
│   │   ├── events.html
│   │   ├── ip.html
│   │   ├── users.html
│   │   └── ...
│   ├── login.html
│   ├── signup.html
│   └── ...
├── parts/                  # Reusable components
│   ├── headerAdmin.html    # Common header
│   ├── footerAdmin.html    # Common footer
│   ├── leftMenu.html       # Navigation menu
│   ├── notification.html   # Alert messages
│   ├── forms/              # Form components
│   ├── panel/              # Side panels
│   ├── tables/             # Data tables
│   ├── widgets/            # Dashboard widgets
│   └── choices/            # Filter dropdowns
└── snippets/               # Code snippets
    ├── php.html
    ├── python.html
    └── nodejs.html

Key patterns:

Internationalization (i18n)

tirreno uses the framework's built-in internationalization support. Language strings are stored in dictionary files under app/Dictionary/.

Using translations in templates:

<h1>{{ @DICT.dashboard_title }}</h1>
<button>{{ @DICT.save_button }}</button>

Using translations in PHP:

$f3 = \Base::instance();

// Get translated string
$message = $f3->get('DICT.welcome_message');

// With variables
$greeting = sprintf($f3->get('DICT.hello_user'), $userName);

Best practices:

JavaScript coding standards

Follow the ESLint configuration in eslint.config.js:

// Use const/let, not var
const API_ENDPOINT = '/sensor/';
let eventCount = 0;

// Use arrow functions
const trackEvent = async (userId, eventType) => {
    const response = await fetch(API_ENDPOINT, {
        method: 'POST',
        body: new URLSearchParams({ userName: userId, eventType }),
    });
    return response.ok;
};

// Use template literals
const message = `User ${userId} logged in at ${timestamp}`;

Page architecture

tirreno uses ES6 modules with a class-based page structure:

import {BasePage} from './Base.js';

import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {IpTypeFilter} from '../parts/choices/IpTypeFilter.js?v=2';
import {IpsChart} from '../parts/chart/Ips.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';

export class IpsPage extends BasePage {

    constructor() {
        super('ips');
        this.initUi();
    }

    initUi() {
        const datesFilter  = new DatesFilter();
        const searchFilter = new SearchFilter();
        const ipTypeFilter = new IpTypeFilter();

        this.filters = {
            dateRange:      datesFilter,
            searchValue:    searchFilter,
            ipTypeIds:      ipTypeFilter,
        };

        const gridParams = {
            url:        `${window.app_base}/admin/loadIps`,
            tileId:     'totalIps',
            tableId:    'ips-table',

            dateRangeGrid:      true,
            calculateTotals:    true,
            totals: {
                type: 'ip',
                columns: ['total_visit'],
            },

            isSortable:         true,
            orderByLastseen:    false,

            choicesFilterEvents: [ipTypeFilter.getEventType()],
            getParams: this.getParamsSection,
        };

        const chartParams = this.getChartParams(datesFilter, searchFilter);

        new IpsChart(chartParams);
        new IpsGrid(gridParams);
    }
}

JavaScript conventions:

PatternDescriptionExample
ES6 modulesUse import/exportimport {BasePage} from './Base.js';
Class inheritancePages extend BasePageclass IpsPage extends BasePage
Version cache-bustingAppend ?v=N to imports'../parts/DatesFilter.js?v=2'
Constructor patternCall super(), then initUi()super('ips'); this.initUi();
Filters objectStore filter instancesthis.filters = { dateRange, searchValue }
Global app baseUse window.app_base for URLs<code contenteditable> </code>${window.app_base}/admin/loadIps ``

JavaScript directory structure:

ui/js/
├── endpoints/                  # Page entry points
│   ├── admin_ips.js
│   ├── admin_events.js
│   └── ...
├── pages/                      # Page controllers
│   ├── Base.js                 # Base page class
│   ├── Ips.js                  # IPs page (IpsPage)
│   ├── Events.js               # Events page
│   └── ...
├── parts/                      # Reusable components
│   ├── DatesFilter.js          # Date range filter
│   ├── SearchFilter.js         # Search input filter
│   ├── DataRenderers.js        # Column rendering functions
│   ├── choices/                # Dropdown filters (Choices.js)
│   │   └── IpTypeFilter.js
│   ├── chart/                  # Chart components (uPlot)
│   │   └── Ips.js
│   ├── grid/                   # Data grid components (DataTables)
│   │   └── Ips.js
│   ├── panel/                  # Detail panels
│   └── utils/                  # Utility modules
│       ├── Constants.js
│       ├── String.js
│       └── Date.js
└── vendor/                     # Third-party libraries

File formatting

Code comments

tirreno uses a minimal documentation style. Write self-documenting code with descriptive names and type declarations. Add comments only to explain "why", not "what":

// Good - explains why
// Skip devices that haven't been updated since last sync
if ($device->lastseen < $lastSync) {
    continue;
}

// Bad - states the obvious
// Check if lastseen is less than lastSync
if ($device->lastseen < $lastSync) {
    continue;
}

Commit messages

Write good commit messages. Follow these guidelines:

Format: <type>: <subject>. Types: Add, Fix, Update, Remove, Refactor, Docs

Add: user session timeout configuration

Allow admins to configure timeout. Default 30 min.
Closes #123

Line endings

All text files should use Unix-style line endings (LF, not CRLF). Windows developers should configure Git: git config --global core.autocrlf input

Testing

Before submitting a pull request:

  1. Test your changes on Chrome and Firefox
  2. Run code quality checks: phpcs, phpstan, eslint
  3. Verify database changes work with PostgreSQL 12+
________________________________________________________________________________