# Server Module Tools

https://dev.wisecp.com/es/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.
- **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;
```

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

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