Folder structure

.github

This folder contains files used for CI/CD on GitHub, such as running tests during merge requests and building Docker images. It streamlines collaboration, automates processes, and ensures consistency across the repository. You'll also find templates for pull requests, issues, and bug reports. To edit the default text in a merge request description, simply modify the relevant file in this folder.

.travis

Refer to https://docs.travis-ci.com/user/for-beginners/

Config

This was introduced with the support of .env files in Cypht

It contains all the contents of the .env file grouped by semantics within the files. Files in this folder return an array. They simply take the values from the .env file and set a default value if it is not defined in .env.

A special case is that the Dynamic.php file (not tracked by git) is automatically generated every time you change something in the .env file. After compiling all other files in this folder, this is the configuration file used by Cypht at runtime.

Related PR: Switching cypht config hm3.* to dotenv

https://github.com/cypht-org/cypht/pull/823/

language

Contain translation files

lib

Contain the core of Cypht

modules

Contains all modules. Each module contains the files below:

You will get a detailed explanation of how each module works in config/app.php.

scripts

Contains script files to generate configuration (done once after installation), create/delete/update user accounts, generate necessary tables to manage users, sessions, or settings based on values in the environment file and finally others script for development purpose.

site

The site folder contains production files generated by scripts/config_gen.php

tests

contains PHPUnit and selenium tests

third_party

Contains third party minified files used in Cypht

.env

The .env file is for high-level configuration. For any variables you're unsure about, check the configuration folder for detailed explanations.

How to

See the new link in the left menu

Sometimes you can add a link to the left menu without seeing it. Cypht caches all menus. You need to click on the reload link below the navigation menu.

Create a page

Cypht supports two types of pages: basic and AJAX. Basic pages are accessible via URL and require a page reload, while AJAX pages load asynchronously.

To create a page called list_messages that displays all messages from the database, navigate to the correct module (e.g., the nux module) and insert the following code just before the return array:

In nux/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 your new page to the routes, the first argument is the name of the page accessible by typing /?page=all_messages in the browser in this case

- add_handler adds a handler to the page. A handler class contains logic, validates forms, binds alert messages, and passes output variables for use in the output.

- add_output adds output to the page. An output class contains HTML that will be returned to the client by typing /?page=all_messages

Handler and Output Modules

In nux/modules.php:

class Hm_Handler_load_messages extends Hm_Handler_Module {
public function process() {
// Do some logic here
// Get the list of messages
// Pass the list to the output
}
}

class Hm_Output_print_messages extends Hm_Output_Module {
protected function output() {
// Get the list of message passed by Hm_Handler_load_messages
// Build the html that will be returned
// Use return statement to return the html
}
}

Let's say you want to add an ajax page called ajax_load_new_messages which will receive new messages sent to the user and update the message list every 15 seconds.

In nux/setup.php:

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

In nux/modules.php:

class Hm_Handler_get_new_messages extends Hm_Handler_Module {
public function process() {
// Do some logic here
// Get the list of new messages
// Pass the list to the output
}
}

class Hm_Output_print_new_messages extends Hm_Output_Module {
protected function output() {
// Get the list of messages passed by Hm_Handler_get_new_messages
// Build the HTML that will be returned
// Use the out to return data
$this->out('ajax_messages', $new_messages);
}
}

Add this code in nux/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);
}
});

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

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

Add third party

Copy the minified file to the third-party directory

Add the file path in Hm_Output_page_js~np~::~/np~output or Hm_Output_header_css~np~::~/np~output if it is a CSS file.

Finally, add the file in the combine_includes function in scripts/config_gen.php so that it is added when generating the production site.

Enable a module

Edit .env file and add your module to CYPHT_MODULES variable

Create a module

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.

Translate string

If you need to translate a string in:

Handler module

Most strings in handlers are only intended to alert the user of success/information/failure operations. And so they are all written without a translation method but they are all translated later in the Hm_Output_msgs::output function.

Output module

In Output module:

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

In JS file

In JS file:

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

The second parameter here is optional because the default language is the user's language in the settings page.

Add new translation string

Add your string to each file contained in the language folder.

The file names in this folder are language codes. And all return an array. Just add your new string to the end of the array. If you know the translation of the text in a language, add it as a value, otherwise add false as a value.

Add new language

Duplicate the en.php file into the language folder

Rename it (Cypht uses 2 digit code, please refer to https://en.wikipedia.org/wiki/List_of_ISO_639_lingual_codes)

In the renamed file, modify interface_lang and interface_direction in the array accordingly.

Add your language in the interface_langs() function.

Update this test to add your language: Hm_Test_Core_Output_Modules::test_lingual_setting

Run test

Cypht has PHPUnit tests and selenium

PHPUnit

To run all tests:

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

To run tests contained in a class or a single test method:

To run tests contained in a class or a single test method:

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

To run Selenium tests, make sure you have Python installed (https://www.python.org/downloads/). Once Python is installed, you should be able to run the following command:

pip install -r requirements.txt

This command will install all required packages. The requirements.txt file is located at tests/selenium, so make sure to specify the correct path in the command line, or navigate to that location.

Once all packages are installed, you can run Selenium tests using this command:

sh runall.sh

Fix PHPUnit failing tests

Run all tests or filter specific ones, and the console will show the results. In VSCode or similar, click on the file path to go directly to the line causing the issue. Check recent changes to classes, divs, or logic in the handlers that may be causing the failure.

There was 1 failure: 1) Hm_Test_Uid_Cache::test_uid_is_read Failed asserting that true is false. /Applications/MAMP/htdocs/cypht/tests/phpunit/cache.php:19

Debug AJAX requests

Cypht relies heavily on AJAX requests. To debug, add var_dump and exit in your code. Open your browser's developer tools, go to the Network tab, filter by Fetch/XHR to see AJAX requests, then run the code. Click on the request you want to inspect and view the preview or response.

The core module

The core module handles:

- Rendering CSS headers

- Displaying alert messages

- Loading JS files and defining shared JS functions

- Configuring basic features like backups, server pages, settings, etc.

Practical Example: 1. Add a test page

As mentioned earlier, we use the setup_base_page function to add a new page. We'll call this function to make our test page available:

In core/setup.php:

setup_base_page('test');

My Cypht project is available locally at cypht.test. Once we have added this page, we should be able to access cypht.test as follows: cypht.test?page=test

You might find it strange that we see "Page Not Found!" ๐Ÿ˜”, but understand that this is completely normal because we have explained the concept of authorizing pages, forms, etc.

In core/setup.php:

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

After authorizing our page, we can now see a blank page. At least we are getting a result, even if it's not yet what we expected ๐Ÿ˜.

As mentioned earlier, the concepts of output and handle are important. At this stage, we define outputs to display the content of our pages. To show content to the user, create outputs containing HTML.

In core/setup.php:

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

The first parameter refers to the page that we defined above: test. The second parameter refers to the class (module) that we need to define in core/modules.php with the name Hm_Output_test_heading (In the definition of the output in setup.php, we are not going to include Hm_Output_ because it is a prefix that will be detected automatically). The third parameter allows you to indicate whether this content will be displayed based on the user's authentication status. The fourth parameter indicates the module that contains the module (output) code. The fifth parameter indicates the position on our page where this content will be displayed, and the sixth parameter, which goes with the fifth, determines whether this content will be before or after the fifth parameter.

Here is Hm_Output_content_section_start, the content after which we want to display our header.

In core/output_modules.php:

class Hm_Output_content_section_start extends Hm_Output_Module {
/**
* Opens a main tag for the primary content section
*/
protected function output() {
return '.$this->trans('Offline').' }

In our case, we want to place our html content before closing:

In core/output_modules.php:

<div class="row m-0 position-relative">
<main class="container-fluid content_cell">

Here is the definition of the module: test_heading which will display the title Test in the header

In core/output_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 we get; we start to see the result that we are looking for ๐Ÿ‘Œ.

Now that we know how to add content, let's add another section after the header.

Define the output in core/setup.php to display it right after the header. You should already know what the penultimate and last parameters will be.

In core/setup.php:

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

Let's define our class(module): test_first_div

In core/output_modules.php:

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:

Let's add another element to our page just after our second element:

In core/setup.php:

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

In core/output_modules.php:

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:

Now that we know how to add content, let's add a form element to the page. We'll submit the form and handle its processing in the backend

In core/setup.php:

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

In core/output_modules.php:

class Hm_Output_test_third_div extends Hm_Output_Module {
protected function output() {
return '<div class="nux_help mt-3 col-lg-12 col-md-12 col-sm-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="subtitle mt-4">Tag name</div>'.
'<div class="form-floating mb-3">'.
'<input required type="text" id="new_tag_name" name="new_tag_name" class="txt_fld form-control" value="" placeholder="'.$this->trans('Tag name').'" />'.
'<label class="" 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>'.
'</div>';
}
}

Here is the result:

We learned that add_output allows us to add HTML content to the page, while add_handler is used for backend processing (like a controller). Both accept similar parameters, with the concepts of 'before' and 'after' applying to handlers instead of outputs. Remember that these refer to other handlers, and the class will extend Hm_Handler_Module

In core/setup.php:

add_handler('test', 'process_test_third_div', true, 'core', 'load_user_data', 'after');

Don't forget to authorize the form field in 'allowed_post'; otherwise, you won't be able to access it.

In core/setup.php:

/* allowed input */
return array(
'allowed_pages' => array(
'...',
'test'
),
'allowed_output' => array(
'...',
),
'allowed_cookie' => array(
'...',
),
'allowed_server' => array(
'...',
),
'allowed_get' => array(
'...',
),
'allowed_post' => array(
'...',
'new_tag_name' => FILTER_DEFAULT
)
);

We can now define our handler:

In core/setup.php:

class Hm_Handler_process_test_third_div extends Hm_Handler_Module {
/**
* If "stay logged in" is checked, set the session lifetime
*/
public function process() {
list($success, $form) = $this->process_form(array('new_tag_name'));
if ($success && $form['new_tag_name']) {
// var_dump($form);die();
}
}
}

In this documentation, we've discussed how to pass data from POST to GET. Let's consider a scenario where we want to display the entered data as the label for a field in our form. This involves using sessions in Cypht:

In core/handler_modules.php:

$this->session->set('key', 'value');

Our code will look like this:

In core/handler_modules.php:

class Hm_Handler_process_test_third_div extends Hm_Handler_Module {
/**
* If "stay logged in" is checked, set the session lifetime
*/
public function process() {
list($success, $form) = $this->process_form(array('new_tag_name'));
if ($success && $form['new_tag_name']) {
$this->session->set('tag_name', $form['new_tag_name']);
}
}
}

Remember: to retrieve our stored data, we need another handler, and it must be placed after the handlers that store the data; otherwise, it may cause malfunctions.

In core/setup.php:

add_handler('test', 'get_test_third_div', true, 'core', 'load_user_data', 'after');

To retrieve an element from the session, we will do it like this:

$res = $this->session->get('key', 'Default Value');

Let's also define the module in handler_modules.php

In core/handler_modules.php:

class Hm_Handler_get_test_third_div extends Hm_Handler_Module {
/**
* If "stay logged in" is checked, set the session lifetime
*/
public function process() {
$res = $this->session->get('tag_name', 'Tag name');
if(!is_null($res)) {
$this->out('tag_name', $res);
// we can now delete session as we do not need no more
$this->session->del('tag_name');
}
}
}

This is how you can delete an element from the session

$this->session->del('key');

In the Hm_Handler_get_test_third_div module, we will never encounter a null value because we provided a default. Therefore, the is_null condition is unnecessary. We use $this->out('tag_name', $res); to pass our data to the output. To retrieve it from the output, we will do it like this:

$this->get('key');

We will have to edit our class Hm_Output_test_third_div like this:

In core/output_modules.php:

class Hm_Output_test_third_div extends Hm_Output_Module {
protected function output() {
$tag_name = $this->get('tag_name');
return '<div class="nux_help mt-3 col-lg-12 col-md-12 col-sm-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="subtitle mt-4">'.$tag_name.'</div>'.
'<div class="form-floating mb-3">'.
'<input required type="text" id="new_tag_name" name="new_tag_name" class="txt_fld form-control" value="" placeholder="'.$this->trans('Tag name').'" />'.
'<label class="" for="new_tag_name">'.$tag_name.'</label>'.
'</div>'.
'<input type="submit" class="btn btn-primary px-5" value="'.$this->trans('Add').'" name="submit_tag" />'.
'</form>'.
'</div>'.
'</div>'.
'</div>';
}
}

Result before:

Result after:

Practical Example: 2. How to add new settings

Let's start by adding a simple parameter and then we'll see how to add another section after.

We will add our setting after this content:

On page settings

Default message sort order

We will need a handle and an output for that:

In setup.php:

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

default_sort_order_setting is the output for 'Default message sort order.' When adding an output, we need to specify its position. In this case, we want to place it after default_sort_order_setting. The process_test_enable_tag_with_parent_setting must come before save_user_settings since we need to save it as well. Thus, our configuration process will be applied and saved before handling save_user_settings.

Now that weโ€™ve defined our routes, we need to write the module for processing: process_test_enable_tag_with_parent_setting for the handle and test_enable_tag_with_parent_setting for the output.

In module.php:

/**
* @subpackage tags/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);
}
}

and this is the corresponding output:

In module.php:

/**
* @subpackage tags/output
*/
class Hm_Output_test_enable_tag_with_parent_setting extends Hm_Output_Module {
protected function output() {
$settings = $this->get('user_settings');
if (array_key_exists('test_enable_tag_with_parent', $settings) && $settings['test_enable_tag_with_parent']) {
$checked = ' checked="checked"';
$reset = '';
}
else {
$checked = '';
$reset = '';
}
return '<tr class="general_setting"><td><label class="form-check-label" for="test_enable_tag_with_parent">'.
$this->trans('Test Tag enable parent').''.
'<td><input class="form-check-input" type="checkbox" '.$checked.
' value="1" id="test_enable_tag_with_parent" name="test_enable_tag_with_parent" />'.$reset.'
}
}

reset_default_value_checkbox class here is being used in js (module core site.js) to restore the default value at line:

In core/site.js:

$('.reset_default_value_checkbox').on("click", reset_default_value_checkbox);

Remember, if you make changes, you need to define the restore action in JavaScript, as this can occur in various scenarios.

You can now refresh your page to see the result:

You might be wondering why updating our parameter gives an unexpected result; this is normal. Let me explain: remember the crucial concepts in Cypht related to authorizations and validations (such as allowed_pages, allowed_output, allowed_post, etc.). In this case, we need to authorize our test_enable_tag_with_parent field in POST.

In core/setup.php:

'allowed_post' => array(
'test_enable_tag_with_parent' => FILTER_VALIDATE_INT
)

By adding this permission, the display will remain unchanged, but the update will work as intended. To access this setting in Cypht, use the usual syntax:

In core/setup.php:

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

Now that we know how to add a simple setting, let's explore how to add a section to the settings page:

The goal is to add a new section to our settings page. Hereโ€™s how:

__~~#F00:INSERT THE IMAGE HERE~~__

In the image above, our section contains two elements, so we will define two handles to process them, along with three outputs: one for the section title, one for the first element, and another for the second element.

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

We can now add modules, let's start with process_tag_source_max_setting:

In tags/modules.php:

/**
* Process "tag_per_source" setting for the tag page in the settings page
* @subpackage core/handler
*/
class Hm_Handler_process_tag_source_max_setting extends Hm_Handler_Module {
/**
* Allowed values are greater than zero and less than MAX_PER_SOURCE
*/
public function process() {
process_site_setting('tag_per_source', $this, 'max_source_setting_callback', DEFAULT_PER_SOURCE);
}
}

and for process_tag_since_setting:

In tags/modules.php:

/**
* Process "since" setting for the junk page in the settings page
* @subpackage core/handler
*/
class Hm_Handler_process_tag_since_setting extends Hm_Handler_Module {
/**
* valid values are defined in the process_since_argument function
*/
public function process() {
process_site_setting('tag_since', $this, 'since_setting_callback');
}
}

That's all for our handles, we need to add output modules too; let's start with "start_tag_settings":

In tags/modules.php:

/**
* Starts the Tag section on the settings page
* @subpackage core/output
*/
class Hm_Output_start_tag_settings extends Hm_Output_Module {
/**
* Settings in this section control the tag messages view
*/
protected function output() {
$res = ''.
''.
$this->trans('Tags').'';
print_r($res);
return $res;
}
}

for "tag_since_setting" add:

In tags/modules.php:

/**
* Option for the "junk since" date range for the Junk page
* @subpackage core/output
*/
class Hm_Output_tag_since_setting extends Hm_Output_Module {
/**
* Processed by Hm_Handler_process_tag_since_setting
*/
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 '
$this->trans('Show junk messages since').''.
''.message_since_dropdown($since, 'tag_since', $this).'';
}
}

and for "tag_per_source_setting" add:

In tags/modules.php:

/**
* Option for the maximum number of messages per source for the Junk page
* @subpackage core/output
*/
class Hm_Output_tag_per_source_setting extends Hm_Output_Module {
/**
* Processed by Hm_Handler_process_tag_source_max_setting
*/
protected function output() {
$sources = DEFAULT_PER_SOURCE;
$settings = $this->get('user_settings', array());
$reset = '';
if (array_key_exists('tag_per_source', $settings)) {
$sources = $settings['tag_per_source'];
}
if ($sources != 20) {
$reset = '';
}
return '<tr class="tag_setting"><td><label for="tag_per_source">'.
$this->trans('Max messages per source').'</label></td>'.
'<td class="d-flex"><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).'" />'.$reset.'</td></tr>';
}
}

And there you have it! Refresh your page to see the result:



Related links:

https://github.com/dovecot/imaptest/wiki/About
https://jmap.io/spec.html
http://sieve.info/
https://p5r.uk/blog/2011/sieve-tutorial.html
https://www.fastmail.com/help/technical/sieve.html
https://docs.gandi.net/en/gandimail/sieve/sieve_tutorial.html

Page on Github
Edit here

Fill out an issue at Github
Submit an issue

Chat with us at Gitter
Cypht at Gitter

Want to contribute?
Cypht website contribution