date('c'), 'source'=>'opus5-page-api-swap', 'actions'=>[]]; $raw = file_get_contents('php://input'); $d = json_decode($raw, true) ?: []; $page = $d['page'] ?? ''; $old = $d['old'] ?? ''; $new = $d['new'] ?? ''; $dry = !empty($d['dry_run']); // Validation stricte if (!$page || !$old || !$new) { http_response_code(400); echo json_encode(['err'=>'missing_params', 'need'=>['page','old','new']]); exit; } // Whitelist pages (sécurité) $whitelist = ['ethica-chatbot.html', 'wevia-master.html', 'wevia-widget.html']; if (!in_array($page, $whitelist)) { http_response_code(403); echo json_encode(['err'=>'page_not_whitelisted', 'whitelist'=>$whitelist]); exit; } // Whitelist API endpoints (pattern /api/*.php seulement) if (!preg_match('#^/api/[a-z0-9\-_]+\.php$#i', $old) || !preg_match('#^/api/[a-z0-9\-_]+\.php$#i', $new)) { http_response_code(400); echo json_encode(['err'=>'invalid_api_pattern', 'expected'=>'/api/*.php']); exit; } // Vérifier que new endpoint existe et est testable $newFile = '/var/www/html' . $new; if (!file_exists($newFile)) { http_response_code(404); echo json_encode(['err'=>'new_endpoint_not_found', 'path'=>$newFile]); exit; } $F = '/var/www/html/' . $page; if (!file_exists($F)) { http_response_code(404); echo json_encode(['err'=>'page_not_found', 'path'=>$F]); exit; } $content_before = file_get_contents($F); $R['page_size_before'] = strlen($content_before); // Compte occurrences $R['old_occurrences'] = substr_count($content_before, $old); if ($R['old_occurrences'] === 0) { http_response_code(404); echo json_encode(['err'=>'old_pattern_not_found', 'old'=>$old]); exit; } // DRY RUN — retourne preview sans exec if ($dry) { $preview = str_replace($old, $new, $content_before); $R['dry_run'] = true; $R['preview_diff'] = $R['old_occurrences'] . ' occurrences replaced'; $R['size_after'] = strlen($preview); echo json_encode($R, JSON_PRETTY_PRINT); exit; } // Check chattr $attr = trim(shell_exec("lsattr $F 2>&1 | awk '{print \$1}'")); $R['chattr_before'] = $attr; $had_immutable = strpos($attr, 'i') !== false; // GOLD backup (doctrine 3) $GOLD = $F . '.GOLD-' . date('Ymd-His') . '-pre-opus5-api-swap'; $gold_ok = @copy($F, $GOLD); $R['gold'] = $gold_ok ? $GOLD : 'FAILED'; if (!$gold_ok) { http_response_code(500); echo json_encode(['err'=>'gold_backup_failed']); exit; } // Remove chattr via sudo (NOPASSWD www-data) if ($had_immutable) { $o = shell_exec("sudo chattr -i $F 2>&1"); $R['chattr_removed'] = trim($o) === '' ? 'OK' : trim($o); if (strpos($R['chattr_removed'], 'Operation not permitted') !== false) { $R['err'] = 'chattr_remove_failed'; echo json_encode($R, JSON_PRETTY_PRINT); exit; } } // Do the swap $content_after = str_replace($old, $new, $content_before); $written = file_put_contents($F, $content_after); $R['written_bytes'] = $written; $R['page_size_after'] = strlen($content_after); // Verify swap ok $verify = file_get_contents($F); $R['new_present_after'] = substr_count($verify, $new); $R['old_remaining'] = substr_count($verify, $old); // Lint HTML basique (check no &1"); $attr_after = trim(shell_exec("lsattr $F 2>&1 | awk '{print \$1}'")); $R['chattr_after'] = $attr_after; } // Rollback si something broken (detection simple) if ($R['new_present_after'] === 0 || strpos($verify, '&1"); @copy($GOLD, $F); if ($had_immutable) shell_exec("sudo chattr +i $F 2>&1"); $R['err'] = 'verification_failed_rollback_done'; $R['rolled_back'] = true; http_response_code(500); echo json_encode($R, JSON_PRETTY_PRINT); exit; } $R['doctrine'] = '66 — swap API endpoint safe (whitelist + GOLD + chattr + verify + rollback)'; $R['success'] = true; echo json_encode($R, JSON_PRETTY_PRINT);