Developer Docs

Welcome to the Cypht developer documentation. This guide covers everything you need to know about contributing to Cypht, from understanding the codebase structure to creating modules and debugging. Cypht is built entirely with a modular architecture using PHP and JavaScript. Every feature is implemented as a module that can be enabled or disabled independently, making it highly extensible and customizable.
Understanding the Cypht folder structure is essential for navigating and modifying the codebase effectively.
.github

CI/CD configuration files, templates for pull requests, issues, and bug reports. Automates testing and Docker image building during merge requests.

Config

Configuration files that work with .env files. Contains grouped settings with default values. Dynamic.php is auto-generated from .env changes.

language

Translation files for internationalization. Each file uses a 2-digit language code and returns an array of translated strings.

lib

Contains the core framework code of Cypht. Essential classes and utilities that power the entire application.

modules

All application modules. Each module contains setup.php, modules.php, site.js, site.css, and potentially an assets folder.

scripts

Utility scripts for configuration generation, user management, database operations, and development tasks.

site

Production files generated by scripts/config_gen.php. Contains optimized and minified assets for deployment.

tests

PHPUnit and Selenium test suites for automated testing and quality assurance.

third_party

Minified third-party libraries and dependencies used by Cypht.

.env

High-level configuration file. Check the Config folder for detailed explanations of all available variables.

Cypht's modular design is the foundation of its extensibility. Each module is self-contained and can be enabled or disabled independently.

Each module contains the following files:

  • site.js: Module-specific JavaScript code
  • site.css: Module-specific CSS styles
  • setup.php: Module configuration, pages, handlers, and allowed variables
  • modules.php: Handler and output classes
  • assets/: Fonts, images, and other resources (optional)

Modules should be designed to be as self-contained as possible. It's acceptable to depend on the core module, but avoid dependencies between non-core modules. If you need functionality from another module, consider if you're building it in the right place.

Module Encapsulation

Avoid require/include lines for files from another module at all costs. Use module execution order ("after" or "before") to augment functionality without direct dependencies.

Cypht supports two types of pages: basic pages (accessible via URL) and AJAX pages (load asynchronously).

To create a page called "list_messages":

In module/setup.php:

setup_base_page('all_messages', 'core');

add_handler('all_messages', 'load_messages', true);
add_output('all_messages', 'print_messages', true);
  • setup_base_page adds the page to the routes: it becomes reachable with /?page=all_messages.
  • add_handler attaches a handler: logic, form validation, alert messages, and output variables.
  • add_output attaches an output: the HTML returned to the client.

In module/modules.php:

class Hm_Handler_load_messages extends Hm_Handler_Module {
    public function process() {
        // Logic to get messages
        $this->out('messages', $message_list);
    }

}

class Hm_Output_print_messages extends Hm_Output_Module {
    protected function output() {
        $messages = $this->get('messages');
        return '<div class="message_list">' . implode('', $messages) . '</div>';
    }
}

For AJAX pages that load data asynchronously:

setup_base_ajax_page('ajax_load_new_messages', 'core');

add_handler('ajax_load_new_messages', 'get_new_messages', true);
add_output('ajax_load_new_messages', 'print_new_messages', true);
Page Authorization

Don't forget to add your new pages to the 'allowed_pages' array in your module's setup.php return statement.

Add this code in module/site.js to run your AJAX page every 15 seconds:

$(function() {
    if (hm_page_name() === 'all_messages') {
        setInterval(function() {
            Hm_Ajax.request(
                [{'name': 'hm_ajax_hook', 'value': 'ajax_load_new_messages'}],
                function(res) {
                    if (res.ajax_messages) {
                        // Append new messages to the list of messages
                        $('#messages_list').append(res.messages);
                    }
                }
            );
        }, 15000);
    }

});
AJAX Note

If setup_base_ajax_page does not have output modules, the values returned with $this->out() will be accessible in res in JavaScript.

Finally, add the following code to module/setup.php:

return array(
    'allowed_pages' => array(
        ...
        'ajax_load_new_messages',
        'all_messages'
    ),
    'allowed_get' => array(...),
    'allowed_output' => array(
        ...
        'ajax_messages'
    ),
    'allowed_post' => array(...)

);
  • Add all_messages and ajax_load_new_messages to the list of allowed pages
  • Add ajax_messages to the list of allowed outputs
  • Add post/get variables if they exist in the list of allowed get/posts
Debug Info

A page can have multiple handlers/outputs. A full list of all handlers and outputs attached to a page can be seen by accessing ?page=info page on your instance in the configuration map section.

Understanding the separation between handlers (logic) and outputs (presentation) is key to Cypht development.
Handler Modules

Contain business logic, form validation, data processing, and prepare data for output. Extend Hm_Handler_Module and implement the process() method.

Output Modules

Generate HTML and handle presentation. Extend Hm_Output_Module and implement the output() method. Use $this->trans() for translations.

  1. Handlers process first and can pass data using $this->out('key', 'value')
  2. Outputs run after handlers and can access data using $this->get('key')
  3. Outputs return HTML that gets combined to form the final page
Cypht supports multiple languages with a simple translation system.

In output modules:

$this->trans("Your text here");

Or with specific language:

hm_trans("Your text here", "en");

The second parameter is optional: the default is the user's language from the settings page.

Strings in handlers

Most strings in handlers only alert the user about success/information/failure. They are written without a translation call because they are all translated later in Hm_Output_msgs::output.

Add your string to every file in the language folder. File names are language codes and each file returns an array: append your string at the end. If you know the translation, add it as the value, otherwise use false.

  1. Duplicate en.php in the language folder
  2. Rename using 2-digit ISO 639 code
  3. Modify interface_lang and interface_direction
  4. Add to interface_langs() function
  5. Update Hm_Test_Core_Output_Modules::test_lingual_setting test
Cypht includes comprehensive test suites using PHPUnit and Selenium to ensure code quality and reliability.

Run all tests:

php vendor/phpunit/phpunit/phpunit --configuration tests/phpunit/phpunit.xml

Run specific tests:

php vendor/phpunit/phpunit/phpunit \
--configuration tests/phpunit/phpunit.xml \
--filter classOrMethodName
  1. Install Python and required packages: pip install -r tests/selenium/requirements.txt
  2. Run tests: sh tests/selenium/runall.sh

Check console output for failure details, click file paths in IDE to navigate to problematic lines, and review recent changes to classes or logic.

There was 1 failure:
1) Hm_Test_Uid_Cache::test_uid_is_read
Failed asserting that true is false.
/var/www/cypht/tests/phpunit/cache.php:19
Effective debugging techniques for Cypht development.
  • Add var_dump() and exit() in your code
  • Use browser developer tools → Network tab
  • Filter by Fetch/XHR to see AJAX requests
  • Click requests to inspect preview/response

If you add a link to the left menu but don't see it, Cypht caches menus. Click the reload link below the navigation menu to refresh.

GlitchTip is an Open Source, Sentry-compatible error tracker. Pointing your instance at one gives you the stack trace, the request context and the number of occurrences for every PHP error, which is the fastest way to investigate intermittent bugs that you cannot reproduce on demand.

Cypht ships the Sentry SDK as a required Composer dependency, so composer install already put everything in place. Reporting stays off until you provide a DSN.

Create a project in GlitchTip, either on the hosted service or on your own instance, and copy the DSN it gives you into your .env file

#Glitchtip errors capturing
GLITCHTIP_DSN=https://<key>@app.glitchtip.com/<project-id>
GLITCHTIP_TRACES_SAMPLE_RATE=0.01

That is the whole setup. On the next request index.php reads the variable and initialises the client only when it holds a value :

$glitchtip_dsn = env('GLITCHTIP_DSN', '');

if ($glitchtip_dsn) {
    \Sentry\init([
        'dsn' => $glitchtip_dsn,
        'traces_sample_rate' => env('GLITCHTIP_TRACES_SAMPLE_RATE', 0.01),
    ]);
}

Leave GLITCHTIP_DSN empty and nothing is loaded, so the feature costs nothing when
you do not use it.

GLITCHTIP_TRACES_SAMPLE_RATE is the percentage of transaction events sent to GlitchTip, so it governs performance traces and not errors: 0.01 samples one percent of them. Errors are always reported, whatever this value is. Keep it low in production to save disk space, and raise it temporarily when you need to profile.

For more details please check the GlitchTip PHP SDK documentation.

Attaching a report to a bug

Enabling it on a development or staging instance is the practical way to turn "it fails from time to time" into an actionable report. Link the GlitchTip issue when you open a ticket so maintainers get the stack trace directly.

Guidelines for integrating third-party libraries and maintaining compatibility with existing integrations.
  1. Copy the minified file to the third-party directory
  2. Add the file path in Hm_Output_page_js.output, or Hm_Output_header_css.output if it is a CSS file
  3. Finally, add the file in the combine_includes function in scripts/config_gen.php so that it is added when generating the production site

Edit .env file and add your module to CYPHT_MODULES variable:

CYPHT_MODULES=core,imap,smtp,your_module_name

In the modules folder, you'll find a hello_world module with the necessary scaffolding for creating a new module. Customize your module by following the code explained above.

Integration Compatibility

Cypht is actively used as embedded webmail in Tiki read the integration code before changing the Cypht codebase. Be careful with refactoring, module updates, layout changes, and interface modifications that might break upstream integrations.

The core module provides essential functionality that other modules depend on.
  • CSS Headers: Renders CSS headers and stylesheets
  • Alert Messages: Displays system alerts and notifications
  • JavaScript Loading: Loads JS files and defines shared functions
  • Basic Features: Configures backups, server pages, settings, etc.
Module Dependencies

Most modules will depend on the core module for basic functionality, but the core module is designed to work independently of all other modules.

Let's build a complete page step by step, from the route to a working form, using everything covered above.

Step 1 : Add the page in core/setup.php :

setup_base_page('test');

Step 2 : Open the page at ?page=test. The route exists, but the page is not authorized yet, so Cypht answers "Page Not Found!" :

Cypht answering Page Not Found on the unauthorized test page

This is expected: every page, form field and output must be explicitly allowed.

Step 3 : Authorize the page in core/setup.php :


return array(
            'allowed_pages' => array(...,'test'),
            'allowed_output' => array(...),
            'allowed_cookie' => array(...),
            'allowed_server' => array(...),
            'allowed_get' => array(...),
            'allowed_post' => array(...)
);

Once the page is authorized we get a blank page a result at last, even if not the one we are after yet:

The Cypht test page rendering blank after being authorized

Step 4 : Add content with outputs in core/setup.php :

add_output('test', 'test_heading', true, 'core', 'content_section_start', 'after');

Step 5 : Define the output class in core/modules.php :

class Hm_Output_test_heading extends Hm_Output_Module {
    protected function output() {
        return '<div class="content_title">'.$this->trans('Test').'</div>';
    }
}

And here is the result: the "Test" title now shows in the page header.

Cypht test page showing the Test title in the header

Step 6 : Add more content with additional outputs:

add_output('test', 'test_first_div', true, 'core', 'test_heading', 'after');
class Hm_Output_test_first_div extends Hm_Output_Module {
    protected function output() {
        return '<div class="mt-3 col-lg-6 col-md-12 col-sm-12">
            <div class="card">
                <div class="card-body">
                    <div class="card_title">
                        <h4>'.$this->trans('Test').'</h4>
                    </div>
                    '.$this->trans('We are just testing').'
                </div>
            </div>
        </div>';
    }

}

And here is the result we hope for a first card right below the header:

Cypht test page with a first content card below the header
The six add_output parameters
  1. page: the page this output belongs to (test).
  2. output name: the class in modules.php, written without the Hm_Output_ prefix (it is auto-detected).
  3. logged_in: whether the output is shown based on the user's authentication status.
  4. module: the module that contains the output code (core).
  5. marker: the existing output to position against.
  6. placement: before or after the marker.

Step 7: Add a second card after the first one :

add_output('test', 'test_second_div', true, 'core', 'test_first_div', 'after');

class Hm_Output_test_second_div extends Hm_Output_Module {
    protected function output() {
        return '<div class="mt-3 col-lg-6 col-md-12 col-sm-12">
            <div class="card">
                <div class="card-body">
                    <div class="card_title">
                        <h4>'.$this->trans('Test again').'</h4>
                    </div>
                    '.$this->trans('We are again just testing').'
                </div>
            </div>
        </div>';
    }
}

And here is the result: the two cards sit side by side.

Cypht test page with a second content card next to the first one

Outputs render HTML; handlers do the backend work (like a controller). Both take similar parameters, but for handlers the before/after ordering refers to other handlers. Handler classes extend Hm_Handler_Module.

Step 8: Add a third output containing a form :

add_output('test', 'test_third_div', true, 'core', 'test_second_div', 'after');

class Hm_Output_test_third_div extends Hm_Output_Module {
    protected function output() {
        return '<div class="nux_help mt-3 col-lg-12">
            <div class="card"><div class="card-body">
                <div class="card_title">
                    <h4>'.$this->trans('Test Our Form').'</h4>
                </div>

                <form class="add_server me-0" method="POST" action="?page=test">
                    <input type="hidden" name="hm_page_key"
                       value="'.$this->html_safe(Hm_Request_Key::generate()).'" />
                    <div class="form-floating mb-3">
                        <input required type="text" id="new_tag_name" name="new_tag_name"
                            class="txt_fld form-control" placeholder="'.$this->trans('Tag name').'" />
                        <label for="new_tag_name">'.$this->trans('Tag name').'</label>
                    </div>
                    <input type="submit" class="btn btn-primary px-5"
                       value="'.$this->trans('Add').'" name="submit_tag" />
                </form>
            </div>
         </div>';
    }
}

Here is the result: the form card is added below the two previous cards.

Cypht test page with a form card below the two content cards

Step 9: Authorize the form field in allowed_post (otherwise it is discarded), then register and define the handler:


// core/setup.php
'allowed_post' => array(
    ...
    'new_tag_name' => FILTER_DEFAULT
);

//
add_handler('test', 'process_test_third_div', true, 'core','load_user_data'
, 'after');
// core/handler_modules.php
class Hm_Handler_process_test_third_div extends Hm_Handler_Module {
    public function process() {
        list($success, $form) = $this->process_form(array('new_tag_name'));
        if ($success && $form['new_tag_name']) {
            // do something with $form['new_tag_name']
        }
    }
}

To carry data between requests for example to show the submitted value back as the field label use the session. Store it in the processing handler:

$this->session->set('tag_name', $form['new_tag_name']);

Read it back in a second handler placed after the one that stored it, pass it to the output with $this->out(), then clear it :

// core/setup.php
add_handler('test', 'get_test_third_div', true, 'core', 'load_user_data', 'after');

// core/handler_modules.php
class Hm_Handler_get_test_third_div extends Hm_Handler_Module {
    public function process() {
        $res = $this->session->get('tag_name', 'Tag name');
        $this->out('tag_name', $res);
        $this->session->del('tag_name'); // no longer needed
    }
}

Finally, read it in the output with $this->get('tag_name') and use it as the field label :

class Hm_Output_test_third_div extends Hm_Output_Module {
    protected function output() {
        $tag_name = $this->get('tag_name');
        // ...use $tag_name as the <label> and subtitle text...
    }
}
Handler ordering matters

The retrieval handler must run after the one that stores the value, otherwise the session key won't exist yet. Because a default is provided to session->get(), the value is never null.

Result before the label is the static "Tag name" string :

Cypht form with the static Tag name label before wiring the session value

Result after the label and subtitle now show the value submitted on the previous request:

Cypht form with the label populated from the session value
Settings are added the same way as page content with a handler (to save the value) and an output (to render the control). Let's add a single setting first, then a whole settings section.

We will add our checkbox right after the built-in "Default message sort order" setting :

Cypht settings page showing the Default message sort order setting

Register a handler and an output in the settings page:


//
add_handler('settings', 'process_test_enable_tag_with_parent', true,
 'tags', 'save_user_settings', 'before');

//
add_output('settings', 'test_enable_tag_with_parent_setting', true, 
'tags', 'default_sort_order_setting', 'after');

The handler must run before save_user_settings so the value is persisted with the rest of the settings.

// tags/modules.php  (handler)
class Hm_Handler_process_test_enable_tag_with_parent_setting 
extends Hm_Handler_Module {
    public function process() {
        function test_tag_with_parent_enabled_callback($val) { return $val; }
        process_site_setting('test_enable_tag_with_parent', 
        $this, 'test_tag_with_parent_enabled_callback', true, true);
    }
}
// tags/modules.php  (output)
class Hm_Output_test_enable_tag_with_parent_setting extends Hm_Output_Module {
    protected function output() {
        $settings = $this->get('user_settings');
        $checked = (array_key_exists('test_enable_tag_with_parent', $settings) 
        && $settings['test_enable_tag_with_parent'])
            ? ' checked="checked"' : '';
        return '<tr class="general_setting"><td><label class="form-check-label" for="test_enable_tag_with_parent">'.
            $this->trans('Test Tag enable parent').'</label></td>'.
            '<td><input class="form-check-input" type="checkbox"'.$checked.
            ' value="1" id="test_enable_tag_with_parent" name="test_enable_tag_with_parent" /></td></tr>';
    }
}

You can now refresh the settings page to see the new checkbox :

Cypht settings page with the new Test Tag enable parent checkbox
Authorize the field in POST

Until the field is allowed in POST, the control renders but the update silently does nothing.

// core/setup.php
'allowed_post' => array(
    'test_enable_tag_with_parent' => FILTER_VALIDATE_INT
)

Read the saved value anywhere with the usual syntax :

$this->user_config->get('test_enable_tag_with_parent_setting');

Now that a single setting works, let's add a whole section to the settings page :

Cypht settings page before adding a dedicated Tags section

The goal is the section below: a title plus two settings.

Target Tags settings section with two settings

It needs two handlers (to process each value) and three outputs (section title + two controls):

// tags/setup.php
add_handler('settings', 'process_tag_source_max_setting', true, 'tags', 'load_user_data', 'after');
add_handler('settings', 'process_tag_since_setting',     true, 'tags', 'load_user_data', 'after');
add_output('settings', 'start_tag_settings',    true, 'tags', 'sent_source_max_setting', 'after');
add_output('settings', 'tag_since_setting',     true, 'tags', 'start_tag_settings',      'after');
add_output('settings', 'tag_per_source_setting', true, 'tags', 'tag_since_setting',      'after');
// tags/modules.php  (handlers)
class Hm_Handler_process_tag_source_max_setting extends Hm_Handler_Module {
    public function process() {
        process_site_setting('tag_per_source', $this, 'max_source_setting_callback', DEFAULT_PER_SOURCE);
    }
}

class Hm_Handler_process_tag_since_setting extends Hm_Handler_Module {
    public function process() {
        process_site_setting('tag_since', $this, 'since_setting_callback');
    }
}
// tags/modules.php  (outputs)
class Hm_Output_start_tag_settings extends Hm_Output_Module {
    protected function output() {
        return '<tr><td data-target=".tag_setting" colspan="2" class="settings_subtitle cursor-pointer border-bottom p-2">'.
            '<i class="bi bi-tags fs-5 me-2"></i>'.$this->trans('Tags').'</td></tr>';
    }
}

class Hm_Output_tag_since_setting extends Hm_Output_Module {
    protected function output() {
        $since = DEFAULT_SINCE;
        $settings = $this->get('user_settings', array());
        if (array_key_exists('tag_since', $settings) && $settings['tag_since']) {
            $since = $settings['tag_since'];
        }
        return '<tr class="tag_setting"><td><label for="tag_since">'.
            $this->trans('Show tag messages since').'</label></td>'.
            '<td>'.message_since_dropdown($since, 'tag_since', $this).'</td></tr>';
    }
}

class Hm_Output_tag_per_source_setting extends Hm_Output_Module {
    protected function output() {
        $sources = DEFAULT_PER_SOURCE;
        $settings = $this->get('user_settings', array());
        if (array_key_exists('tag_per_source', $settings)) {
            $sources = $settings['tag_per_source'];
        }
        return '<tr class="tag_setting"><td><label for="tag_per_source">'.
            $this->trans('Max messages per source').'</label></td>'.
            '<td><input type="text" size="2" class="form-control form-control-sm w-auto" '.
            'id="tag_per_source" name="tag_per_source" value="'.$this->html_safe($sources).'" /></td></tr>';
    }
}

And there you have it! Refresh the settings page to see the new Tags section :

Cypht settings page with the new Tags section and its two settings