Fix literal parsing of multiline Unraid notifications

This commit is contained in:
Mikei386
2026-09-21 19:16:54 +02:00
parent 1c41175a86
commit 5c76877bd7
3 changed files with 80 additions and 2 deletions
+11
View File
@@ -331,6 +331,17 @@ bash scripts/package.sh
### Lokaler Test ### Lokaler Test
Die PHP-Statushelper lassen sich unabhängig vom laufenden MUA-Dienst testen
(PHP 8 erforderlich):
```bash
php scripts/test-notifications.php
php scripts/test-docker-update-status.php
```
Der Benachrichtigungsparser liest auch mehrzeilige URBM-Meldungen und erhält
Text wörtlich, ohne INI-Variablen wie `${HOME}` auszuwerten.
```bash ```bash
# Test-Setup (ohne Unraid) # Test-Setup (ohne Unraid)
MUA_CONFIG_DIR=/tmp/mua-test MUA_PORT=3999 MUA_CONFIG_PORT=3998 ./dist/mua-mac MUA_CONFIG_DIR=/tmp/mua-test MUA_PORT=3999 MUA_CONFIG_PORT=3998 ./dist/mua-mac
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/unraid-mcp-status-helper.php';
set_error_handler(function ($severity, $message) { throw new RuntimeException($message); });
function expect_same(string $label, mixed $expected, mixed $actual): void {
if ($expected !== $actual) throw new RuntimeException('FAIL: ' . $label);
echo "PASS: $label\n";
}
$record = <<<'NOTIFY'
timestamp=1789870052
event="URBM"
subject="✅ Backup (nightly)"
description="First line
📦 Repository: Test
Literal "quotes", semicolons; equals=signs
importance=alert
${PATH} ${HOME} null true OFF
✅ Status: Erfolgreich"
importance="normal"
NOTIFY;
$fields = notification_fields($record);
expect_same('timestamp stays literal', '1789870052', $fields['timestamp']);
expect_same('multiline Unicode and punctuation', "First line\n\n📦 Repository: Test\nLiteral \"quotes\", semicolons; equals=signs\nimportance=alert\n" . '${PATH} ${HOME} null true OFF' . "\n✅ Status: Erfolgreich", $fields['description']);
expect_same('embedded key is description text', 'normal', $fields['importance']);
expect_same('CRLF records', $fields, notification_fields(str_replace("\n", "\r\n", $record)));
expect_same('empty field and raw scalars', ['event'=>'', 'subject'=>'OFF', 'timestamp'=>'001'], notification_fields("event=\"\"\nsubject=OFF\ntimestamp=001\n"));
expect_same('comments and whitespace', ['event'=>'test'], notification_fields("; comment\n# comment\n\nevent=\"test\" \n"));
expect_same('truncated record is rejected', [], notification_fields("event=\"URBM\"\ndescription=\"unfinished\n"));
expect_same('malformed record is rejected', [], notification_fields("not a record\n"));
expect_same('empty record', [], notification_fields(''));
echo "All notification parser tests passed.\n";
+36 -2
View File
@@ -170,14 +170,48 @@ function disk_health(?string $requested): array {
return ['schema_version' => '1.0', 'disk_count' => count($result), 'disks' => $result]; return ['schema_version' => '1.0', 'disk_count' => count($result), 'disks' => $result];
} }
/** Read Unraid's flat notification records without INI value interpolation.
* INI_SCANNER_RAW rejects multiline quoted descriptions; INI_SCANNER_NORMAL
* expands ${...} and coerces bare values. Notification text must stay literal.
*/
function notification_fields(string $contents): array {
$fields = [];
$pending = null;
$value = '';
foreach (preg_split('/\r\n|\n|\r/', $contents) as $line) {
if ($pending !== null) {
$value .= "\n" . $line;
} else {
if (trim($line) === '' || preg_match('/^\s*[;#]/', $line)) continue;
if (!preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/', $line, $match)) return [];
$pending = $match[1];
$value = ltrim($match[2]);
if (!str_starts_with($value, '"')) {
$fields[$pending] = rtrim($value);
$pending = null;
continue;
}
}
$end = rtrim($value, " \t");
if (strlen($end) >= 2 && str_ends_with($end, '"')) {
$fields[$pending] = substr($end, 1, -1);
$pending = null;
}
}
// Never return a partial record when a quoted field is truncated.
return $pending === null ? $fields : [];
}
function notification_list(int $limit, ?string $importance): array { function notification_list(int $limit, ?string $importance): array {
$limit = max(1, min(50, $limit)); $limit = max(1, min(50, $limit));
$files = array_merge(glob('/tmp/notifications/*.notify') ?: [], glob('/tmp/notifications/archive/*.notify') ?: []); $files = array_merge(glob('/tmp/notifications/*.notify') ?: [], glob('/tmp/notifications/archive/*.notify') ?: []);
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
$items = []; $items = [];
foreach ($files as $file) { foreach ($files as $file) {
$n = parse_ini_file($file, false, INI_SCANNER_RAW); $contents = @file_get_contents($file);
if (!is_array($n)) continue; if ($contents === false) continue; // Notifications may be archived/deleted concurrently.
$n = notification_fields($contents);
if (!$n) continue;
$level = trim((string)($n['importance'] ?? 'normal'), '"'); $level = trim((string)($n['importance'] ?? 'normal'), '"');
if ($importance && $importance !== 'all' && $level !== $importance) continue; if ($importance && $importance !== 'all' && $level !== $importance) continue;
$items[] = [ $items[] = [