Server Module Tools
Tools are the pages a customer gets inside a service: file manager, databases, DNS, cron jobs. The base class owns everything around them; you supply two methods and a template.
Overview
ServerModule ships a catalogue of tool descriptors, all switched off. Turning one on means declaring it supported, then answering two questions: what the page shows, and what a button does.
Everything between the browser and those two answers belongs to the base class.
tool_data(), normalizes rows, runs the filter hook, caches per request.
tool_action(), writes the service history with secrets masked.
Prerequisites
- A working server module (Writing a Server Module). Tools open only after
create()stores the account identity. - Confirm on the live provider that the main entity supports both create and delete.
- If the tool creates something the provider bills for, plan the quota.
Structure
Six layers, three of them yours.
| Layer | Where | What it holds |
|---|---|---|
| Catalogue | coremio/classes/ServerModule.php | Descriptor, filters, rules, columns, row callbacks |
| Enabling the tool | Your config.php or configure_features() | Supported tools and capabilities |
| Read | Your module, tool_data() | One case per tool, then a fetch method |
| Write | Your module, tool_action() | One case per tool, then an action router |
| Interface | templates/system/module/service/hosting/tools | Shared template per tool slug |
| Text | coremio/locale/en/cm/system/module.php | Shared labels and messages |
Walkthrough
Enable the Tool
Declaring the slug switches the tool on with its default capabilities.
// config.php: the declarative half.
return [
// ... the rest of the module configuration
'supported' => [
'tools' => ['file-manager', 'ftp-accounts', 'databases', 'cron-jobs'],
// Per tool option overrides, the same keys set_tool() would write.
'tool_options' => [
'file-manager' => ['allow_upload_overwrite' => true, 'allow_chmod_recursive' => true],
],
],
];
public function configure_features(): void
{
// Same effect as the config list, useful when the decision depends on live state.
$this->support_tools(['cron-jobs' => ['list', 'create', 'delete']]);
// The provider has no per account quota and no e-mail field on cron jobs,
// so remove the capabilities we cannot serve, plus the column they feed.
$this->remove_tool_capability('ftp-accounts', ['quota']);
$this->remove_tool_column('ftp-accounts', ['quota']);
$this->set_tool_order('databases', 5);
}
Read Path
tool_data() is a single dispatcher; keep the per tool work in private fetch methods.
- Return
['items' => [...]]keyed by column name, plus any extra key the template reads.
Write Path
tool_action() has the same shape, with a router matching on the action verb.
- Throw on refusal; the message reaches the customer as a red alert.
- Return
[], or a payload when the interface needs a value back.
Columns and Text
Column titles, labels and messages come from the shared language file. The template is shared, so a field name is a contract. Provider specific strings go in your module language file.
Reference
Tool Descriptor
$this->tools['ftp-accounts'] = [
'name' => 'FTP Accounts',
// Group names depend on the module type declared in config.php:
// hosting: files, databases, domains, email, software, security
// server: power, system, network, storage
'group' => 'files',
'order' => 20, // position inside the group
'icon' => 'bi bi-hdd-network', // a Bootstrap icon, or 'img:<url>' for a remote glyph
'page' => 'ftp-accounts', // template file name under tools/
'type' => 'page-loader', // 'page-loader' renders a page, 'action' is a single button
'supported' => false, // your module flips this on
'capabilities' => ['list', 'create', 'edit', 'delete', 'quota', 'directory'],
];
false. A request for an unsupported tool is refused before your code runs.
'page-loader' loads the template named by page. 'action' is a single button in the tool grid, optionally with 'confirm' => true.
get_content and save_content count as edit; update_email counts as email.
Module Signatures
// The two the base class calls. $params carries the sanitized query parameters,
// $data carries the sanitized POST body.
public function tool_data(string $tool, string $action = 'index', array $params = []): array;
public function tool_action(string $tool, string $action, array $data = []): array;
// Optional: adjust the catalogue for this module. Runs once the server record is
// bound (from the constructor, through set_server) and again from set_service.
public function configure_features(): void;
// Optional: only needed when the reset-password tool must let the panel invent one.
protected function panel_generated_password(): string;
Base Class Helpers
public function support_tools(array $tools): static;
public function add_tool(string $key, array $config): static;
public function set_tool(string $key, array $config): static;
public function remove_tool(string $key): static;
public function set_tool_order(string $key, int $order): static;
public function add_tool_capability(string $key, array|string $capabilities): static;
public function remove_tool_capability(string $key, array|string $capabilities): static;
public function add_tool_column(string $tool, string $column, array $config): static;
public function remove_tool_column(string $tool, array|string $columns): static;
public function add_tool_group(string $key, array $config): static;
public function set_tool_group_order(string $key, int $order): static;
public function get_tool(string $key): ?array;
public function get_tools(): array;
public function get_effective_tools(): array;
public function get_disabled_features(): array;
A plain list element turns the tool on with its default capabilities. A key pointing at an array replaces that list with what you passed.
Validation Layers
All three are declared in the base class and run before your action method.
// 1. Sanitizing. Any field not listed falls back to 'hclear'.
protected function get_tool_action_data_filters(): array;
// The shipped rule for this tool, verbatim. Note that an FTP user name is filtered
// as an e-mail, because panels accept the user@domain form:
// 'ftp-accounts' => ['username' => 'email', 'password' => null,
// 'directory' => 'path', 'quota' => 'numeric']
// Filters: hclear, numeric, route, identifier, domain, subdomain, hostname,
// email, email_list, ip, url, path, json_filenames, null (pass through)
// 2. Required fields, per action. An empty string after trimming throws.
protected function get_tool_action_rules(): array;
// 'ftp-accounts' => ['create' => ['username', 'password'],
// 'edit' => ['username'], 'delete' => ['username']]
// 3. Format checks, run after the required check and skipped for empty values.
protected function get_tool_action_field_validations(): array;
// 'mx-entry' => ['create' => ['domain' => ['domain'], 'priority' => ['numeric']]]
// Rules: url, email, email_list, email_local, ip, ipv4, ipv6, ip_or_wildcard,
// domain, dns_label, numeric, cron_field, enum:a,b,c
Every other filter strips the characters that make a password strong: the account then gets a secret the customer never typed.
tool_action() Returns
action-success-{tool}-{action}, then action-success-{action}, then a generated label.
Template Variables
/** @var ServerModule $module the live instance, so $module->service is reachable */
/** @var string $tool the slug */
/** @var array $tool_config the descriptor, including capabilities and options */
/** @var string $tool_label the translated tool name */
/** @var string $action 'index' unless the page was opened on a sub action */
/** @var array $data exactly what your fetch method returned */
/** @var mixed $table the prepared listing table, or null when the tool has no columns */
/** @var array $tables sub tables, keyed by their own slug */
/** @var bool $admin_view true in the admin panel, false in the client area */
/** @var string|null $error set when the fetch failed */
| Browser function | Purpose |
|---|---|
request_tool_action(tool, action, data, options) | Posts one action, with spinner and toast |
reload_module_content(tool) | Reloads the tool page after a change |
open_modal(id, {title, body, footer}) | Builds and opens a dialog |
confirmDeleteModal({message, description, buttonText, onConfirm}) | The standard delete confirmation |
watchRequired(selector) | Enables submit once required inputs are filled |
passwordInput(id, placeholder, options) | Password field with generate, reveal and copy |
Example
One tool end to end: fetch, router, and the base class that consumes both.
public function tool_data(string $tool, string $action = 'index', array $params = []): array
{
$username = $this->options['config']['user'] ?? '';
$domain = $this->options['domain'] ?? '';
return match ($tool) {
'ftp-accounts' => $this->fetch_ftp_accounts($username, $domain),
'databases' => $this->fetch_databases($username),
default => [],
};
}
private function fetch_ftp_accounts(string $username, string $domain): array
{
$response = $this->api->call('ftp/list', ['username' => $username]);
$items = [];
foreach ($response['data'] ?? [] as $row)
$items[] = [
'username' => $row['user'] ?? '',
'directory' => $row['dir'] ?? '/',
'quota' => (int) ($row['quota'] ?? 0),
];
// 'items' feeds the table; anything else is read by the template.
return ['items' => $items, 'domain' => $domain];
}
public function tool_action(string $tool, string $action, array $data = []): array
{
$username = $this->options['config']['user'] ?? '';
return match ($tool) {
'ftp-accounts' => $this->action_ftp_accounts($action, $data, $username),
default => throw new Exception($this->lang['err-tool-unknown']),
};
}
private function action_ftp_accounts(string $action, array $data, string $username): array
{
return match ($action) {
'create' => $this->ftp_create($data, $username),
'delete' => $this->ftp_delete($data, $username),
default => throw new Exception($this->lang['err-action-unknown']),
};
}
private function ftp_create(array $data, string $username): array
{
$this->api->call('ftp/create', [
'account' => $username,
// Already sanitized by the declared filters, and already checked for presence.
'user' => $data['username'] ?? '',
'password' => $data['password'] ?? '',
'directory' => $data['directory'] ?? '/',
], 'POST');
// Empty array: the base class writes the translated success message.
return [];
}
// ServerModule::handle_tool_action, reduced to the sequence that matters.
$tool = Filter::init("REQUEST/tool", "route");
$action = Filter::init("REQUEST/action", "route") ?: 'index';
$data = !empty($_POST) ? $_POST : $_GET;
unset($data['operation'], $data['method'], $data['tool'], $data['action']);
$data = $this->sanitize_tool_action_data($tool, $data);
$tool_config = $this->get_tool($tool);
if (!$tool_config) throw new \Exception('Tool not found');
if (empty($tool_config['supported'])) throw new \Exception('Tool not supported');
$this->check_tool_capability($tool_config, $action);
$this->validate_tool_action($tool, $action, $data);
$result = $this->tool_action($tool, $action, $data);
// Secrets are masked before the action reaches the service history.
foreach ($data as $k => $v)
if (is_string($v) && $v !== '' && preg_match('/pass(word)?|secret|token/i', (string) $k))
$data[$k] = '***';
if (empty($result)) return $this->tool_action_success_response($tool, $action);
return $result;
var TOOL = 'ftp-accounts';
window.ftpCreateSubmit = function (btn) {
request_tool_action(TOOL, 'create', {
username: document.getElementById('ftpUser').value,
password: document.getElementById('ftpPass').value,
directory: document.getElementById('ftpDir').value,
}, {
button: btn,
buttonLoader: creating_loader,
successToast: true,
afterDone: function () {
close_modal(document.querySelector('.modal.show'));
reload_module_content(TOOL);
},
});
};
Pitfalls
The interface draws a control for every default capability. Remove what your router does not handle, or the customer clicks Edit and gets "Action not available".
An open create button turns curiosity into your invoice. Tie the resource to an addon and refuse the action once the allowance is used up.
The helper puts the tool action in the body, then merges your data over it. A field called action replaces the verb and the dispatcher throws. Prefix it (task_action) in the template, the required rules and your action method.
The helper handles that key in the same tab, before your callback finishes. Use a neutral name such as console_url for a URL you want in a dialog or a new tab.
A tool whose main entity the provider cannot both create and delete is not shipped. Take it out of the configuration and delete the dead methods.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.