# Server Module Tools

https://dev.wisecp.com/uk/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.

- **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.
- **has_client_management()**: Whether the customer gets a Management tab: true while an effective tool, or a card the client area keeps, remains.
- **capability**: One verb the tool offers: permission gate and interface flag.

## Prerequisites

- A working server module ([Writing a Server Module](https://dev.wisecp.com/en/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.

```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],
        ],
    ],
];
```

```php
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

```php
$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

```php
// 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

```php
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;
public function has_client_management(): bool;
```

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

```php
// 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

```php
/** @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.

```php
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 [];
}
```

```php
// 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;
```

```javascript
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.

> **A module without tools gets no Management tab**
> 
> The customer's Management tab opens only while `has_client_management()` is true: at least one effective tool, or a card the client area keeps. Gauges, account info, resources and links sit on the Overview tab, so they do not count. The core asks with the operator's restrictions applied, so hiding every tool in Preferences removes the tab too. A panel that offers only single sign-on keeps its launcher in Quick Actions.

## Related Articles

- [Writing a Server Module](https://dev.wisecp.com/en/writing-a-server-module)
- [Interface Components](https://dev.wisecp.com/en/interface-components)
- [Filtering User Input](https://dev.wisecp.com/en/filtering-user-input)
- [The Client Area Bridge](https://dev.wisecp.com/en/the-client-area-bridge)
- [Translations and Language Files](https://dev.wisecp.com/en/translations-and-language-files)
- [Product Module Client Management](https://dev.wisecp.com/en/product-module-client-management)
