# The Client Area Bridge

https://dev.wisecp.com/es/the-client-area-bridge

Call a method on your addon module from the customer's browser. The operation already exists: no controller, no route, no endpoint of your own.

## Overview

An addon page in the client area is HTML your module produced. Then it needs to do something: save a preference, fetch a status, start a job. One operation is already wired on the website, and it calls any method whose name starts with `use_`.

The admin panel has the same bridge behind the tools controller. What differs is who is allowed through, and that difference is the security part of this article.

## Prerequisites

- An Addon module with `status` true in its configuration; a disabled addon is invisible to the bridge.
- A web face: the client area opt-in (`show_on_clientArea` plus `clientArea()`) or a public `main()` page. Without one the bridge refuses every call.
- `coremio/modules/Addons/SampleAddon`, a working demonstration of both faces.

## Structure

- **controllers/website/addon.php**: The website face: resolves `/addon/{Name}`, produces the page and dispatches the bridge operation.
- **operations/ClientAddon.php**: The trait holding `use_addon_method`, plus the gate that decides whether your addon has a web face.
- **operations/AdminTools.php**: The admin twin: same operation name and prefix rule, behind an administrator session.
- **AddonModule**: Your base class. Supplies `$area_link`, `$error`, `$config`, `$lang`, `$dir`, `$url` and `view()`.

```text
browser  POST /addon/MyAddon   operation=use_addon_method & method=save-preference
   |
   +-- addon controller  ->  addon_ctx()     enabled config + module instance + web face?
   +-- ClientAddon       ->  member session required when the face is the client area
   +-- name normalised   ->  'save-preference'  becomes  use_save_preference
   +-- method_exists     ->  refuse when absent
   +-- $module->use_save_preference()          called with NO arguments
   +-- falsy return      ->  Exception($module->error)
   '-- array or string   ->  JSON body
```

## Walkthrough

### Give the Addon a Web Face

1. Set `show_on_clientArea` to true in `config.php` and add a `clientArea()` method returning `['page_title' => …, 'breadcrumbs' => …, 'content' => …]`. That makes `/addon/{Name}` the customer page and requires a signed-in member.
2. Or add a `main()` method for a public page, which carries no session guarantee at all.
3. Set `meta.slug` for a pretty address; the controller rewrites `$area_link` to it.

### Write the use_ Method

1. Name it `use_{something}`; nothing else is callable, and that prefix is the whole boundary at the dispatch layer.
2. Take no parameters. Read your input yourself with `Filter::init("POST/…")`, as an operation does.
3. Return a non-empty array or string; a falsy return counts as failure and raises an exception carrying `$this->error`.
4. For a real failure, throw: the caller converts it into the standard error envelope.

### Call It from the Page

1. Post to `$this->area_link`, which the controller already points at the correct address. Never hand-write the path.
2. Send `operation=use_addon_method` and `method={name without the prefix}`, plus whatever else your method reads.
3. On the website use plain `fetch` with the `X-Requested-With` header; in the admin panel use `WcpRequest`.

## Reference

### The Two Endpoints

| Face | Address | Who gets through |
| --- | --- | --- |
| client area | `/addon/{Name}` or the configured slug | a signed-in member, if the addon opted in |
| public page | `/addon/{Name}` | **anyone**, with no session |
| legacy alias | `/addon/{Name}/client` | as the client area; 404 without the opt-in |
| admin panel | the tools addons address | an administrator with the tools privilege |
| admin-only addon called from the website | any of the above | nobody: the operation throws `Addon not found.` |

> **The web face check is a real security boundary, added after a real hole**
> 
> Before it existed, an admin-only addon's `use_*` methods were reachable from the website with no session. Settings were overwritten, ticket data read, paid API credit burned. Treat it as the outer wall, not the only one.

### The Operation and the Name Rule

```php
public function use_addon_method(Operation $operation): bool;
```

```php
$method = (string) Filter::init("REQUEST/method", "route");        // keeps a-zA-Z0-9 - _ .
$method = "use_" . str_replace([' ', '-', '.'], '_', $method);     // spaces, hyphens, dots -> underscore

if ($method === "use_" || !method_exists($module, $method))
    throw new \Exception("Undefined addon method.");

$result = $module->{$method}();                                     // NO arguments
if (!$result) throw new \Exception((string) (($module->error ?? '') ?: 'An error occurred'));

return $operation->output($result);
```

So `method=save-preference`, `save.preference` and `save_preference` all reach `use_save_preference()`. The `route` filter runs first, keeping only `a-zA-Z0-9`, hyphen, underscore and dot, so a name cannot escape into another class.

### What Your Method Must Return

```php
public function use_sample_method(): array|string;
```

| You return | The customer receives | Use it for |
| --- | --- | --- |
| a non-empty array | that array, JSON encoded | the normal case; keep the standard keys |
| a non-empty string | the string, written as is | a ready HTML fragment for the DOM |
| `[]`, `''`, `false` or `null` | `{"status":"error","message":"…"}` built from `$this->error` | nothing: a legacy path, do not design for it |
| a thrown exception | `{"status":"error","message":"your message"}` | every real failure |

The envelope to aim for matches the rest of the panel:

```php
return [
    'status'  => 'successful',
    'message' => $this->lang['saved'] ?? 'Saved.',
    'data'    => ['preference' => $value, 'updated_at' => time()],
];
```

### What the Base Class Hands You

```php
class AddonModule
{
    public string|bool $error = '';        // read by the bridge when your method returns falsy
    public array  $config    = [];         // config.php merged with the saved settings
    public array  $lang      = [];         // lang/{selected}.php
    public string $area_link = '';         // the address to post to; REWRITTEN per face
    public string $_name     = '';         // the directory name
    public array  $user      = [];         // the signed-in member, when there is one
    public array  $admin     = [];         // the signed-in administrator, when there is one
    public string $url       = CORE_FOLDER . DS . MODULES_FOLDER . DS . 'Addons' . DS;
    public string $dir;                    // filesystem path of the module directory
    // The constructor appends {Name} to $url and resolves it into a public URL, fills $dir,
    // $config, $lang, $user and $admin, and points $area_link at the current face.

    public function __construct();
    protected function view($file = '', $variables = []): string;
    public function privileges();
    public function save_settings($pFields, $accessPs): bool;
    public function change_addon_status($arg = '');
    public function save_config($data = []): bool;
    public function use_default_settings($formElements = null);
    public function isEnabled();
}
```

> **area_link is not one fixed value**
> 
> In the panel it points at the tools addons address. On the website the addon controller overwrites it with the public address. Print it rather than building a path.

### Reading Core Data

```php
// $groupAction is "Group/Action" from the route registry; prefix it with "client:" for the
// customer registry, in which case $input must also carry owner_id.
public static function internal(string $groupAction, array $input = [], array $query = []): array;
```

```php
use WISECP\Api\Kernel;

// In process: no HTTP, no authentication, no rate limit, but the SAME envelope the
// external API returns. $input fills the path parameters first, then the body.
$resp    = Kernel::internal('Tickets/GetTicketMessages', ['id' => $ticketId], ['limit' => 50]);
$replies = $resp['data'] ?? [];
```

Prefer this over calling a helper directly: the resource layer hands back decrypted, allow-listed and normalised rows.

## Example

A client page that stores one customer preference.

```php
<?php
namespace WISECP\Modules\Addons\MyAddon;

use Exception;
use Filter;
use Language;
use User;
use UserManager;

class MyAddon extends \AddonModule
{
    /** Rendered at /addon/MyAddon once show_on_clientArea is true in config.php. */
    public function clientArea(): array
    {
        $member = UserManager::LoginData('member');

        return [
            'page_title'  => $this->lang['meta']['name'] ?? 'My Addon',
            'breadcrumbs' => [['link' => '', 'title' => $this->lang['meta']['name'] ?? 'My Addon']],
            'content'     => $this->view('client.php', [
                'link'       => $this->area_link,                                  // post here
                'preference' => (string) (User::getInfo((int) $member['id'], ['my_addon_pref'])['my_addon_pref'] ?? ''),
            ]),
        ];
    }

    /**
     * Reached as method=save-preference. No parameters: it reads its own input, exactly
     * like an operation, and it re-reads the account from the session rather than trusting
     * anything the request sent.
     *
     * @throws Exception
     */
    public function use_save_preference(): array
    {
        $member = UserManager::LoginData('member');
        if (!$member) throw new Exception(Language::gc('website/index/addon-login-required') ?: 'Login required.');

        $value = Filter::init('POST/preference', 'route');
        if ($value === '' || !in_array($value, ['daily', 'weekly', 'never'], true))
            throw new Exception($this->lang['err-bad-preference'] ?? 'Choose one of the offered options.');

        User::AddInfo((int) $member['id'], ['my_addon_pref' => $value]);

        return [
            'status'  => 'successful',
            'message' => $this->lang['saved'] ?? 'Saved.',
            'data'    => ['preference' => $value],
        ];
    }
}
```

```html
<select id="ma-pref" class="form-select">
    <option value="daily">Daily</option>
    <option value="weekly">Weekly</option>
    <option value="never">Never</option>
</select>
<button type="button" id="ma-save" class="btn btn-primary mt-2">Save</button>

<script>
(function () {
    var link = ;
    var btn  = document.getElementById('ma-save');
    var sel  = document.getElementById('ma-pref');
    if (!btn || !sel) return;

    btn.addEventListener('click', function () {
        btn.disabled = true;

        var body = new URLSearchParams({
            operation:  'use_addon_method',
            method:     'save-preference',   // hyphen is fine, it becomes use_save_preference
            preference: sel.value
        });

        fetch(link, {
            method:  'POST',
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
            body:    body
        })
        .then(function (r) { return r.json(); })
        .then(function (r) {
            if (r.status === 'successful') window.WCPTheme && window.WCPTheme.toast(r.message, 'success');
            else window.WCPTheme && window.WCPTheme.toast(r.message, 'error');
        })
        .finally(function () { btn.disabled = false; });
    });
})();
</script>
```

The same call from an admin page:

```javascript
WcpRequest(AREA_LINK, {
    method: 'POST',
    data:   { operation: 'use_addon_method', method: 'save-preference', preference: 'weekly' },
    button: runBtn,
    buttonLoader: window.saving_loader,
    done: function (response) {           // omit `done` and the standard handling applies
        output.textContent = JSON.stringify(response, null, 2);
    }
});
```

## Pitfalls

> **A public page makes every use_ method public too**
> 
> An addon whose web face is `main()` is reachable with no session by design, for compatibility with older modules. Ask, for each method, who should be able to call it. Write that check in the method.

> **Do not trust an account id that arrives in the request**
> 
> The bridge authenticates the visitor, not the record. A method that reads a customer id from the body serves another customer's data to whoever asks. Read the account from the member session.

> **An empty successful result reads as a failure**
> 
> Returning `[]` after a legitimately empty query produces an error envelope carrying whatever is in the error property, often an empty message. Return an array whose data field is the empty list.

> **Saving through the settings operation overwrites status and access privileges**
> 
> The standard settings save writes the status flag and access privilege list along with your fields, so a custom save that reuses it can disable your addon. Give the custom save its own `use_` method.

> **A disabled addon answers nothing**
> 
> The context resolver checks the enabled flag before it builds the instance, so a call against a disabled addon fails with `Addon not found.`, not a method error.

## Related Articles

- [Writing an Addon Module](https://dev.wisecp.com/en/writing-an-addon-module)
- [Exposing API Endpoints](https://dev.wisecp.com/en/exposing-api-endpoints)
- [Adding an Admin Page](https://dev.wisecp.com/en/adding-an-admin-page)
- [Operations](https://dev.wisecp.com/en/operations)
- [Filtering User Input](https://dev.wisecp.com/en/filtering-user-input)
- [The Client Area](https://dev.wisecp.com/en/the-client-area)
