false, 'error'=>'invalid token']); exit; } $path = $input['path'] ?? ''; $content_b64 = $input['content_b64'] ?? ''; $do_backup = $input['backup'] ?? true; $lint = $input['lint'] ?? null; // --- Path validation --- if (!$path || !$content_b64) { echo json_encode(['ok'=>false, 'error'=>'missing path or content_b64']); exit; } $realpath = realpath(dirname($path)) . '/' . basename($path); $allowed_roots = ['/var/www/html/', '/opt/weval-l99/', '/opt/wevads/']; $ok = false; foreach ($allowed_roots as $r) { if (strpos($path, $r) === 0) { $ok = true; break; } } if (!$ok) { echo json_encode(['ok'=>false, 'error'=>'path outside allowed roots: ' . implode(',', $allowed_roots)]); exit; } $content = base64_decode($content_b64, true); if ($content === false) { echo json_encode(['ok'=>false, 'error'=>'invalid base64 content']); exit; } // --- Step 1: GOLD backup (mandatory) --- $gold = null; if ($do_backup && file_exists($path)) { $vault = '/opt/wevads/vault/'; if (!is_dir($vault)) @mkdir($vault, 0755, true); $gold = $vault . basename($path) . '.GOLD-' . date('Ymd-His') . '-pre-safe-write'; @copy($path, $gold); if (!file_exists($gold)) { // Try via sudo @shell_exec('sudo cp ' . escapeshellarg($path) . ' ' . escapeshellarg($gold) . ' 2>&1'); } } // --- Step 2: Detect immutable --- $lsattr_out = @shell_exec('lsattr ' . escapeshellarg($path) . ' 2>&1'); $was_immutable = ($lsattr_out && strpos($lsattr_out, '-i-') !== false); // --- Step 3: Lint check (if applicable) --- $lint_result = null; if ($lint) { $tmp_lint = '/tmp/wevia-safe-write-lint-' . uniqid() . '.tmp'; file_put_contents($tmp_lint, $content); if ($lint === 'php') { $lint_result = @shell_exec('php8.4 -l ' . escapeshellarg($tmp_lint) . ' 2>&1'); if (strpos($lint_result, 'No syntax errors') === false) { @unlink($tmp_lint); echo json_encode(['ok'=>false, 'error'=>'php lint failed', 'lint_output'=>trim($lint_result), 'gold_backup'=>$gold]); exit; } } elseif ($lint === 'js') { $lint_result = @shell_exec('node -c ' . escapeshellarg($tmp_lint) . ' 2>&1'); if (trim($lint_result) !== '') { @unlink($tmp_lint); echo json_encode(['ok'=>false, 'error'=>'js lint failed', 'lint_output'=>trim($lint_result), 'gold_backup'=>$gold]); exit; } } @unlink($tmp_lint); } // --- Step 4: chattr -i if immutable --- if ($was_immutable) { @shell_exec('sudo chattr -i ' . escapeshellarg($path) . ' 2>&1'); } // --- Step 5: Atomic write via temp + sudo cp --- $tmp = '/tmp/wevia-safe-write-' . uniqid() . '.tmp'; $bytes = file_put_contents($tmp, $content); if ($bytes === false) { echo json_encode(['ok'=>false, 'error'=>'failed write tmp file', 'gold_backup'=>$gold]); exit; } $cp_out = @shell_exec('sudo cp ' . escapeshellarg($tmp) . ' ' . escapeshellarg($path) . ' 2>&1'); $chown_out = @shell_exec('sudo chown www-data:www-data ' . escapeshellarg($path) . ' 2>&1'); @unlink($tmp); // --- Step 6: chattr +i restore if was immutable --- if ($was_immutable) { @shell_exec('sudo chattr +i ' . escapeshellarg($path) . ' 2>&1'); } // --- Verify --- $final_size = file_exists($path) ? filesize($path) : 0; $success = ($final_size === strlen($content)); echo json_encode([ 'ok' => $success, 'path' => $path, 'bytes_written' => $bytes, 'final_size' => $final_size, 'was_immutable' => $was_immutable, 'gold_backup' => $gold, 'lint_result' => $lint_result ? trim($lint_result) : null, 'cp_out' => trim($cp_out), 'ts' => date('c') ], JSON_PRETTY_PRINT);