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
statustrue in its configuration; a disabled addon is invisible to the bridge. - A web face: the client area opt-in (
show_on_clientAreaplusclientArea()) or a publicmain()page. Without one the bridge refuses every call. coremio/modules/Addons/SampleAddon, a working demonstration of both faces.
Structure
/addon/{Name}, produces the page and dispatches the bridge operation.
use_addon_method, plus the gate that decides whether your addon has a web face.
$area_link, $error, $config, $lang, $dir, $url and view().
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
- Set
show_on_clientAreato true inconfig.phpand add aclientArea()method returning['page_title' => …, 'breadcrumbs' => …, 'content' => …]. That makes/addon/{Name}the customer page and requires a signed-in member. - Or add a
main()method for a public page, which carries no session guarantee at all. - Set
meta.slugfor a pretty address; the controller rewrites$area_linkto it.
Write the use_ Method
- Name it
use_{something}; nothing else is callable, and that prefix is the whole boundary at the dispatch layer. - Take no parameters. Read your input yourself with
Filter::init("POST/…"), as an operation does. - Return a non-empty array or string; a falsy return counts as failure and raises an exception carrying
$this->error. - For a real failure, throw: the caller converts it into the standard error envelope.
Call It from the Page
- Post to
$this->area_link, which the controller already points at the correct address. Never hand-write the path. - Send
operation=use_addon_methodandmethod={name without the prefix}, plus whatever else your method reads. - On the website use plain
fetchwith theX-Requested-Withheader; in the admin panel useWcpRequest.
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. |
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
public function use_addon_method(Operation $operation): bool;
$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
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:
return [
'status' => 'successful',
'message' => $this->lang['saved'] ?? 'Saved.',
'data' => ['preference' => $value, 'updated_at' => time()],
];
What the Base Class Hands You
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();
}
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
// $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;
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
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],
];
}
}
<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:
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
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.
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.
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.
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.
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
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.