Server Module Tools

6 Aufrufe Markdown

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.

get_tool_data() The read path: validates the descriptor, calls your tool_data(), normalizes rows, runs the filter hook, caches per request.
handle_tool_action() The write path: sanitizes and validates each field, checks the capability, calls your tool_action(), writes the service history with secrets masked.
create_tool_table() Builds the listing table from the declared columns and the row callback.
capability One verb the tool offers: permission gate and interface flag.

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.

LayerWhereWhat it holds
Cataloguecoremio/classes/ServerModule.phpDescriptor, filters, rules, columns, row callbacks
Enabling the toolYour config.php or configure_features()Supported tools and capabilities
ReadYour module, tool_data()One case per tool, then a fetch method
WriteYour module, tool_action()One case per tool, then an action router
Interfacetemplates/system/module/service/hosting/toolsShared template per tool slug
Textcoremio/locale/en/cm/system/module.phpShared labels and messages

Walkthrough

Enable the Tool

Declaring the slug switches the tool on with its default capabilities.

config.php
// 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],
        ],
    ],
];
configure_features()
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.

  1. 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.

  1. Throw on refusal; the message reaches the customer as a red alert.
  2. 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

a catalogue entry
$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'],
];
supported Ships as false. A request for an unsupported tool is refused before your code runs.
capabilities Both the permission list and the interface flag; checked before a button appears.
type 'page-loader' loads the template named by page. 'action' is a single button in the tool grid, optionally with 'confirm' => true.
options Per tool switches the template reads. Set from the configuration or at runtime.
capability aliases get_content and save_content count as edit; update_email counts as email.

Module Signatures

exact 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

catalogue mutators
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;
support_tools() reads its argument two ways

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.

signature and shape
// 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
A password field must be declared with the null filter

Every other filter strips the characters that make a password strong: the account then gets a secret the customer never typed.

tool_action() Returns

[] Ordinary success: the base class looks up action-success-{tool}-{action}, then action-success-{action}, then a generated label.
['status' => 'successful', 'message' => '...'] Success with your own wording, returned as is.
['stream' => ...] A file download; the base class streams it and the request ends there.
['redirect' => ...] Reserved: immediate full page navigation, same tab.
throw Failure. The message reaches the customer, so write it translated.

Template Variables

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 functionPurpose
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.

the module
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 [];
}
the base class
// 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;
the browser side
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

An unimplemented capability is a button that throws

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".

A billable resource needs a quota gate

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.

Never name a form field action, operation, method or tool

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.

redirect navigates away immediately

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 listing only tool is not shipped

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.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.