Route every conf writer through the guarded path
Eleven call sites wrote master.conf with tmp+rename and nothing else — no backup, no parse check, no audit — including the two toggles the UI uses most and the raw editor that installs a whole hand-edited file.
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/confform.php';
|
||||
|
||||
define('VV_DOCKER_JSON', SCRIPTS_DIR . '/docker_folders.json');
|
||||
define('VV_FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
|
||||
@@ -344,17 +345,19 @@ function vv_dk_rename_folder(string $folderId, string $newName): array {
|
||||
$data[$folderId]['name'] = $newName;
|
||||
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
||||
|
||||
// Update conf: replace old folder name with new name in the map
|
||||
// Update conf: replace old folder name with new name in the map.
|
||||
// The rebuild is a pure function of the current contents, so it runs inside vv_conf_edit()'s
|
||||
// lock rather than against a copy read beforehand — there is no window to lose an edit in.
|
||||
$currentHost = vv_detect_host();
|
||||
$myId = strtoupper($currentHost);
|
||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
||||
$map = vv_dk_read_conf_map($raw, $myId);
|
||||
foreach ($map as &$v) {
|
||||
if ($v === $oldName) $v = $newName;
|
||||
}
|
||||
unset($v);
|
||||
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
||||
vv_write_conf_raw($currentHost . '.conf', $updated);
|
||||
vv_conf_edit($currentHost . '.conf', function (string $raw) use ($myId, $oldName, $newName): string {
|
||||
$map = vv_dk_read_conf_map($raw, $myId);
|
||||
foreach ($map as &$v) {
|
||||
if ($v === $oldName) $v = $newName;
|
||||
}
|
||||
unset($v);
|
||||
return vv_dk_write_conf_map($raw, $myId, $map);
|
||||
}, [], ["{$myId}_DOCKER_FOLDER_MAP"]);
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
@@ -367,14 +370,14 @@ function vv_dk_delete_folder(string $folderId): array {
|
||||
unset($data[$folderId]);
|
||||
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
||||
|
||||
// Remove from conf map
|
||||
// Remove from conf map — rebuilt inside the lock, see vv_dk_rename_folder() above.
|
||||
$currentHost = vv_detect_host();
|
||||
$myId = strtoupper($currentHost);
|
||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
||||
$map = vv_dk_read_conf_map($raw, $myId);
|
||||
$map = array_filter($map, fn($v) => $v !== $folderName);
|
||||
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
||||
vv_write_conf_raw($currentHost . '.conf', $updated);
|
||||
vv_conf_edit($currentHost . '.conf', function (string $raw) use ($myId, $folderName): string {
|
||||
$map = vv_dk_read_conf_map($raw, $myId);
|
||||
$map = array_filter($map, fn($v) => $v !== $folderName);
|
||||
return vv_dk_write_conf_map($raw, $myId, $map);
|
||||
}, [], ["{$myId}_DOCKER_FOLDER_MAP"]);
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
@@ -421,22 +424,24 @@ function vv_dk_sync_json_to_conf(): array {
|
||||
$data = vv_dk_read_json();
|
||||
$currentHost = vv_detect_host();
|
||||
$myId = strtoupper($currentHost);
|
||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
||||
_vv_dk_sync_conf_from_json($data, $raw, $currentHost, $myId);
|
||||
_vv_dk_sync_conf_from_json($data, $currentHost, $myId);
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
// Internal: rebuild conf map from current json state and write it
|
||||
function _vv_dk_sync_conf_from_json(array $data, string $raw = '', string $host = '', string $id = ''): void {
|
||||
// Internal: rebuild conf map from current json state and write it.
|
||||
// The contents to splice into are read inside vv_conf_edit()'s lock. The caller used to be able
|
||||
// to hand in a copy it had already read; that was only ever an optimisation, and passing a stale
|
||||
// copy would have written the rest of the conf back as it looked before the lock was taken.
|
||||
function _vv_dk_sync_conf_from_json(array $data, string $host = '', string $id = ''): void {
|
||||
if (!$host) $host = vv_detect_host();
|
||||
if (!$id) $id = strtoupper($host);
|
||||
if (!$raw) $raw = vv_read_conf_raw($host . '.conf');
|
||||
|
||||
$map = [];
|
||||
foreach ($data as $f) {
|
||||
$name = $f['name'] ?? '';
|
||||
foreach ($f['containers'] ?? [] as $c) $map[$c] = $name;
|
||||
}
|
||||
$updated = vv_dk_write_conf_map($raw, $id, $map);
|
||||
vv_write_conf_raw($host . '.conf', $updated);
|
||||
|
||||
vv_conf_edit($host . '.conf', fn(string $raw): string => vv_dk_write_conf_map($raw, $id, $map),
|
||||
[], ["{$id}_DOCKER_FOLDER_MAP"]);
|
||||
}
|
||||
|
||||
@@ -598,50 +598,56 @@ function vv_conf_flag_value(string $name): bool {
|
||||
}
|
||||
|
||||
// Write a boolean flag value to master.conf.
|
||||
// Goes through vv_conf_edit() for the lock, the pre-write backup, the syntax check and the audit
|
||||
// line. This used to call vv_write_conf_raw() directly, which gave it tmp+rename atomicity and
|
||||
// nothing else — no backup, and no check that the file still sourced afterwards.
|
||||
function vv_conf_flag_set(string $name, bool $value): bool {
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
$content = file_get_contents($confPath);
|
||||
if ($content === false) return false;
|
||||
$val = $value ? 'true' : 'false';
|
||||
$new = preg_replace(
|
||||
'/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
|
||||
'${1}' . $val . '${3}',
|
||||
$content, -1, $count
|
||||
);
|
||||
if (!$count) return false;
|
||||
// tmp+rename — every script sources master.conf, so a truncated write here is a
|
||||
// system-wide outage, not a lost toggle.
|
||||
return vv_write_conf_raw('master.conf', $new);
|
||||
if (!vv_conf_key_valid($name)) return false;
|
||||
$val = $value ? 'true' : 'false';
|
||||
|
||||
return vv_conf_edit('master.conf', function (string $content) use ($name, $val): ?string {
|
||||
$new = preg_replace(
|
||||
'/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
|
||||
'${1}' . $val . '${3}',
|
||||
$content, -1, $count
|
||||
);
|
||||
// A name that matches no true/false line is a caller error, not an already-correct
|
||||
// state — unlike the membership toggle below, where absence genuinely means nothing
|
||||
// to do. Returning null keeps the write from happening and logs reason=no-match.
|
||||
return $count ? $new : null;
|
||||
}, [$name => $val]);
|
||||
}
|
||||
|
||||
// Comment or uncomment a script's line in the first master.conf array that contains it.
|
||||
// Goes through vv_conf_edit() for the lock, the pre-write backup, the syntax check and the audit
|
||||
// line — see vv_conf_flag_set() above for what that replaced. There is no key to verify here,
|
||||
// so the audit subject is the script id and a clean source is the whole assertion.
|
||||
function vv_conf_toggle_script(string $rel, bool $enable): bool {
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) return false;
|
||||
$changed = false;
|
||||
$inArray = false;
|
||||
$relEsc = preg_quote($rel, '/');
|
||||
foreach ($lines as &$line) {
|
||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if (!$inArray) continue;
|
||||
if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;
|
||||
$isCommented = (bool)preg_match('/^\s*#/', $line);
|
||||
if ($enable && $isCommented) {
|
||||
$line = preg_replace('/^(\s*)#\s*("' . $relEsc . ')/', '$1$2', $line);
|
||||
$changed = true;
|
||||
} elseif (!$enable && !$isCommented) {
|
||||
$line = preg_replace('/^(\s*)("' . $relEsc . ')/', '$1# $2', $line);
|
||||
$changed = true;
|
||||
return vv_conf_edit('master.conf', function (string $content) use ($rel, $enable): ?string {
|
||||
$lines = preg_split('/(?<=\n)/', $content) ?: [];
|
||||
$changed = false;
|
||||
$inArray = false;
|
||||
$relEsc = preg_quote($rel, '/');
|
||||
foreach ($lines as &$line) {
|
||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if (!$inArray) continue;
|
||||
if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;
|
||||
$isCommented = (bool)preg_match('/^\s*#/', $line);
|
||||
if ($enable && $isCommented) {
|
||||
$line = preg_replace('/^(\s*)#\s*("' . $relEsc . ')/', '$1$2', $line);
|
||||
$changed = true;
|
||||
} elseif (!$enable && !$isCommented) {
|
||||
$line = preg_replace('/^(\s*)("' . $relEsc . ')/', '$1# $2', $line);
|
||||
$changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
unset($line);
|
||||
if (!$changed) return true;
|
||||
// tmp+rename — every script sources master.conf, so a truncated write here is a
|
||||
// system-wide outage, not a lost toggle.
|
||||
return vv_write_conf_raw('master.conf', implode('', $lines));
|
||||
unset($line);
|
||||
// A script in no array has nothing to toggle and the conf already reads the way the
|
||||
// caller asked. Returning the content unchanged reports success without a write.
|
||||
return $changed ? implode('', $lines) : $content;
|
||||
}, [], [$rel]);
|
||||
}
|
||||
|
||||
// Parse an orchestrator script to find which child scripts it calls.
|
||||
|
||||
Reference in New Issue
Block a user