feat: document plugin failures and fix the Terminal plugin + macro dialog crash

Diagnosed two independent faults reported from the running editor.

1. Terminal plugin (market) did not work as shipped.
   Root cause: it is a port of Fluidbyte/Codiad-Terminal and calls Common::execute(),
   which Atheos REMOVED (renamed to raw_execute with a different return shape).
     plugin calls : Common::execute(...)      -> MISSING
     available    : Common::raw_execute(...)  -> ["code"=>int, "output"=>array]
   Fixed to raw_execute + implode of the output array. Verified against the real
   method: pwd/echo return correctly, and after chdir it reports the blog repo path,
   so the terminal opens in the right directory.

   Two further upstream defects found and deliberately NOT fixed, but documented so
   nobody trusts them: the block-list is dead code (`explode(",", BLOCKED === true)`
   passes a boolean, so ssh/telnet are never blocked), and `strtolower($command ===
   "exit")` means typing exit does not clear the auth flag.

   Security note recorded: this plugin is a browser shell as the php-fpm user, gated
   by a hardcoded in-app constant (PASSWORD="terminal"). It is not a secret. Strongest
   argument yet for keeping the container LAN-only.

2. Settings -> Macro dialog returned HTTP 500.
   Root cause: Atheos's own components/macro/dialog.php:35 does implode(", ",
   $macro["fTypes"]) which requires an ARRAY, but class.macro.php::save() writes the
   STRING "N/A" for folder macros. So ANY folder-type macro makes that dialog fatal:
     PHP Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of
     type ?array, string given in .../components/macro/dialog.php:35
   Our writer macros are all folder-scoped, so adding them exposed it.

   Fixed by correcting the DATA (fTypes ""N/A"" -> "[]") rather than patching Atheos's
   code, so the fix survives an Atheos update. Verified implode() now succeeds for all
   8 macros. The trade-off and the proper upstream fix are both documented.

Also records the diagnostic order for any future plugin problem, and the rule of
thumb: a Codiad-era plugin's Common:: calls are probably stale.

Adds:
  docs/known-issues.md                  both faults, root causes, fixes, verification
  patches/Terminal-terminal.php.patched the patched plugin file
  files/macros.db.php                   regenerated with the corrected fTypes
This commit is contained in:
2026-09-29 16:19:34 +10:00
parent 47b1860623
commit 5ff6a06e82
3 changed files with 320 additions and 6 deletions
+159
View File
@@ -0,0 +1,159 @@
# Known Issues & Patches
Upstream Atheos is an actively-moving project with **no releases**, and its plugin market ships
plugins that are **ports from Codiad** (Atheos's predecessor). Two of those plugins use APIs that
Atheos has since removed or changed. This file records what we hit, what we changed, and why the
fix is done the way it is.
Pinned Atheos commit: `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611).
---
## 1. 🔴 Plugin market: the **Terminal** plugin does not work as shipped
**Symptom:** installed the Terminal plugin from the market; it failed / did not function.
**Root cause — a removed API.** `plugins/Terminal/terminal.php` calls `Common::execute(...)`, which
**no longer exists** in Atheos (it was renamed):
```
plugin calls : Common::execute(...) -> MISSING
available : Common::raw_execute(...) -> returns ["code" => int, "output" => array]
Common::safe_execute(...)
```
The plugin's header says it is a port of `Fluidbyte/Codiad-Terminal`, so it targets the old API. Its
return-shape assumption is also stale (the old `execute()` returned `text`; `raw_execute()` returns
`output` as an array).
**Fix applied** (`patches/Terminal-terminal.php.patched`):
```diff
- $output = Common::execute("pwd");
+ $output = Common::raw_execute("pwd");
- $_SESSION["activeDir"] = array([$_SESSION["project"]] => $output["text"]);
+ $_SESSION["activeDir"] = array([$_SESSION["project"]] => implode("\n", (array)$output["output"]));
- $output = Common::execute($cmd);
- return $output["text"];
+ $output = Common::raw_execute($cmd);
+ return implode("\n", (array)$output["output"]);
```
**Verified** against the real method (`php83 -l` clean; both call patterns exercised):
| Call | Result |
|---|---|
| `raw_execute("pwd")` | `code=0`, `output=["/root",""]` |
| `raw_execute("echo ...")` | `code=0`, returns the text |
| after `chdir` to the repo | returns `/var/www/atheos/workspace/blog` — so the terminal opens in the blog |
**Re-apply after an Atheos update** — `scripts/update-atheos.sh` replaces the application files, so
this patch is lost on update. Re-apply it, or drop the plugin.
### Two further defects in the same plugin (NOT fixed — upstream, and mostly harmless)
1. **`terminal.php:95`** — `$blocked = explode(",", BLOCKED === true);` passes a **boolean** to
`explode()`. This evaluates to `explode(",", false)`, so the intended block-list
(`define("BLOCKED","ssh,telnet")`) **never actually blocks anything**. The plugin's own guard is
dead code. Cosmetic in effect (the command would just fail on its own), but worth knowing rather
than trusting.
2. **`terminal.php:124`** — `if (strtolower($command === "exit"))` passes a boolean to
`strtolower()`. Should be `strtolower($command) === "exit"`. Result: typing `exit` does not clear
`term_auth`; the session stays authenticated until the browser session ends.
Left as-is: they are upstream bugs, and fixing them changes the plugin's behaviour beyond the
minimum needed to make it run. Recorded here so nobody "fixes" them by accident or trusts the
block-list.
### ⚠️ Security note
This plugin gives a **browser terminal that executes shell commands as the php-fpm user**
(`atheos`) — and its own password gate is a **hardcoded, in-app constant**:
```php
define("PASSWORD", "terminal"); // terminal.php:24 — you type this once per session
```
It is not a stored credential, but it is not a secret either: anyone who reaches the Login page can
look up the source and unlock it. **This is the strongest argument yet for keeping the container
LAN-only.** It also means plugin installation should be treated as a **privilege decision**, not a
convenience one — see `docs/security-notes.md` §2 (S-7 covers disabling `exec`/`proc_open`, which
would neutralise macros *and* this plugin).
---
## 2. 🟠 The Macro dialog crashed when folder-scoped macros existed
**Symptom:** the Settings → Macro dialog returned HTTP 500.
**Root cause — invalid data by Atheos's own design.** `components/macro/dialog.php:35` does:
```php
implode(", ", $macro["fTypes"]) // requires an ARRAY
```
but `components/macro/class.macro.php` `save()` writes the **string** `"N/A"` for folder macros:
```php
"fTypes" => $type === "file" ? explode(",", $fTypes) : "N/A",
```
So **any folder-type macro** makes that dialog fatal:
```
PHP Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array,
string given in /var/www/atheos/components/macro/dialog.php:35
```
**Fix applied — correct the DATA, not Atheos's code.** The stored `fTypes` for folder macros is now
`[]` (an empty array) instead of `"N/A"`:
```sh
sed -i 's/"fTypes": "N\/A"/"fTypes": []/g' /var/www/atheos/data/macros.db.php
```
Verified: `implode()` now succeeds for all 8 macros.
**Why data rather than code:** patching `dialog.php` would be overwritten by `update-atheos.sh`
(and by any Atheos update), whereas valid data keeps working. The trade-off is that the UI's own
"save" path could re-introduce `"N/A"` if a macro is edited in the UI — see the follow-up below.
**Follow-up (not done):** the real upstream fix is
`$type === "file" ? explode(",", $fTypes) : []` in `class.macro.php::save()`. Applying it would stop
the UI re-introducing the bad value, at the cost of a patch that must be re-applied after updates.
Worth doing if the dialog starts 500-ing again after editing a macro in the UI.
---
## 3. Checking for plugin problems in future
The plugin's JS is bundled into `public/plugins.min.js` (built by `classes/sourcemanager.php`), and
its rightbar button is rendered by `components/sidebars/sb-right.php` from `plugin.json`. Both were
verified working:
```
plugins.min.js contains 'plugins/Terminal/init.js' -> YES (bundle is fine)
rightbar HTML -> <a onclick="atheos.Terminal.open();">
<i class="fas fa-terminal"></i>Terminal</a>
```
So when a plugin misbehaves, check in this order:
```sh
# 1. did it install?
tail /var/www/atheos/data/log/market.log.php
# 2. is its JS in the served bundle?
grep -c "plugins/<Name>/init.js" /var/www/atheos/public/plugins.min.js
# 3. is it a stale-API port? (Codiad-era plugins are the risk)
grep -rhoE 'Common::[a-zA-Z_]+' /var/www/atheos/plugins/<Name>/ | sort -u
# then check each exists: grep -rn "function <name>" /var/www/atheos/common.php /var/www/atheos/traits/
# 4. is there a PHP fatal?
grep -E 'PHP Fatal' /var/log/nginx/atheos.error.log | tail
```
**Rule of thumb:** if a plugin was ported from Codiad, assume its `Common::` calls are stale and
verify them before trusting it.
+6 -6
View File
@@ -25,7 +25,7 @@
"uuid": "macro-writer-0001-start",
"title": "1. Start/Join Writer Branch",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && (git checkout atheos-writer 2>/dev/null || git checkout -b atheos-writer) && git push -u origin atheos-writer",
"icon": "fas fa-code-branch",
"owner": "armistace"
@@ -34,7 +34,7 @@
"uuid": "macro-writer-0002-commit",
"title": "2. Commit + Push Writer Branch",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git add -A && (git diff --cached --quiet || git commit -m \"writer: $(date +%F_%H%M)\") && git push origin atheos-writer",
"icon": "fas fa-cloud-upload-alt",
"owner": "armistace"
@@ -43,7 +43,7 @@
"uuid": "macro-writer-0003-reset",
"title": "3. DESTRUCTIVE: Reset Writer Branch to Master",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && git fetch origin && git reset --hard origin/master && git push --force-with-lease origin atheos-writer",
"icon": "fas fa-exclamation-triangle",
"owner": "armistace"
@@ -52,7 +52,7 @@
"uuid": "macro-writer-0004-status",
"title": "4. Status + Recent Log",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && git status --short --branch && echo '--- recent commits:' && git log --oneline -8 && echo '--- local branches:' && git branch",
"icon": "fas fa-info-circle",
"owner": "armistace"
@@ -61,7 +61,7 @@
"uuid": "macro-writer-0005-refresh",
"title": "5. Refresh Master (ff-only)",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && git checkout master && git pull --ff-only && git checkout atheos-writer",
"icon": "fas fa-sync-alt",
"owner": "armistace"
@@ -70,7 +70,7 @@
"uuid": "macro-writer-0006-pullmaster",
"title": "6. Merge Master Into Writer Branch",
"type": "folder",
"fTypes": "N/A",
"fTypes": [],
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git fetch origin && git merge origin/master -m \"merge master into atheos-writer\" && git push origin atheos-writer",
"icon": "fas fa-code-merge",
"owner": "armistace"
+155
View File
@@ -0,0 +1,155 @@
<?php
//////////////////////////////////////////////////////////////////////////////80
// Atheos Terminal
//////////////////////////////////////////////////////////////////////////////80
// Copyright (c) 2020 Liam Siira (liam@siira.io), distributed as-is and without
// warranty under the MIT License. See [root]/docs/LICENSE.md for more.
// This information must remain intact.
//////////////////////////////////////////////////////////////////////////////80
// Copyright (c) 2013 Codiad & Kent Safranski
// Source: https://github.com/Fluidbyte/Codiad-Terminal
//////////////////////////////////////////////////////////////////////////////80
require_once("../../common.php");
//////////////////////////////////////////////////////////////////////////////80
// Verify Session or Key
//////////////////////////////////////////////////////////////////////////////80
Common::checkSession();
//////////////////////////////////////////////////////////////////////////////80
// Globals
//////////////////////////////////////////////////////////////////////////////80
$project = SESSION("project");
define("PASSWORD", "terminal");
define("ROOT", Common::getWorkspacePath($project));
define("BLOCKED", "ssh,telnet");
// define("ENABLED", "java,ls,cd,echo,javac");
define("ENABLED", false);
//////////////////////////////////////////////////////////////////////////////80
// Terminal Class
//////////////////////////////////////////////////////////////////////////////80
class Terminal {
//////////////////////////////////////////////////////////////////////////80
// Constructor
//////////////////////////////////////////////////////////////////////////80
public function __construct() {
if (!isset($_SESSION["activeDir"]) || !isset($_SESSION["activeDir"][$_SESSION["project"]]) || empty($_SESSION["activeDir"][$_SESSION["project"]])) {
if (ROOT === "") {
$output = Common::raw_execute("pwd");
$_SESSION["activeDir"] = array([$_SESSION["project"]] => implode("\n", (array)$output["output"]));
} else {
$this->changeDir(ROOT);
}
} else {
$this->changeDir($_SESSION["activeDir"][$_SESSION["project"]]);
}
}
//////////////////////////////////////////////////////////////////////////80
// Primary call
//////////////////////////////////////////////////////////////////////////80
public function process($str) {
$cmd = $this->parseCommand($str);
if ($cmd === "") return;
$output = Common::raw_execute($cmd);
return implode("\n", (array)$output["output"]);
}
//////////////////////////////////////////////////////////////////////////80
// Parse command for special functions, blocks
//////////////////////////////////////////////////////////////////////////80
public function parseCommand($str) {
// Explode command
$command_parts = explode(" ", $str);
// Handle "cd" command
if (in_array("cd", $command_parts)) {
$cd_key = array_search("cd", $command_parts);
$cd_key++;
$dir = $command_parts[$cd_key];
$this->changeDir($dir);
// Remove from command
$str = str_replace("cd $dir", "", $str);
}
// Replace text editors with cat
$editors = array("vim", "vi", "nano");
$str = preg_replace("/^(".join("|", $editors).")/", "cat", trim($str));
if (ENABLED) {
// Handle enabled commands
$enabled = explode(",", ENABLED);
if (in_array($command_parts[0], $enabled) === false) {
$str = "echo ERROR: Command not allowed";
}
} else {
// Handle blocked commands
$blocked = explode(",", BLOCKED === true);
if (in_array($command_parts[0], $blocked)) {
$str = "echo ERROR: Command not allowed";
}
}
// Update exec command
return $str . " 2>&1";
}
//////////////////////////////////////////////////////////////////////////80
// Chnage Directory
//////////////////////////////////////////////////////////////////////////80
public function changeDir($dir) {
chdir($dir);
// Store new directory
$_SESSION["activeDir"][$_SESSION["project"]] = exec("pwd");
}
}
//////////////////////////////////////////////////////////////////////////////80
// Processing
//////////////////////////////////////////////////////////////////////////////80
$activeUser = SESSION("user");
$command = POST("command");
if (strtolower($command === "exit")) {
$_SESSION["term_auth"] = false;
$output = "[EXIT]";
} else if (!isset($_SESSION["term_auth"]) || $_SESSION["term_auth"] !== true) {
if ($command === PASSWORD) {
$_SESSION["term_auth"] = true;
$output = "[AUTHENTICATED]";
} else {
$output = "Enter Password:";
}
} else {
$Terminal = new Terminal();
$output = "";
$command = explode(" && ", $command);
debug($command);
foreach ($command as $c) {
$output .= $Terminal->process($c);
// $output .= implode("\n",$Terminal->process($c));
}
}
$output = array(
"data" => htmlentities($output),
"dir" => htmlentities(exec("pwd")),
"prompt" => "<span class=\"user\">$activeUser</span>:<span class=\"path\">" . exec("pwd") . "</span>$ "
);
Common::send("success", $output);
?>