body: partial ad fields → update * DELETE save.php?id= → delete * POST save.php?action=reorder body: {ids:[…]} → reorder * POST save.php?action=toggle body: {id,active} → toggle active */ require_once __DIR__ . '/config.php'; header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); function ad_ok(mixed $data, int $code = 200): never { http_response_code($code); echo json_encode(['ok' => true, 'data' => $data], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } function ad_err(string $msg, int $code = 400): never { http_response_code($code); echo json_encode(['ok' => false, 'error' => $msg]); exit; } // ── Auth ────────────────────────────────────────────────────────────────────── $supplied = trim($_SERVER['HTTP_X_AD_KEY'] ?? $_SERVER['HTTP_X_Ad_Key'] ?? ($_GET['ad_key'] ?? '')); if ($supplied === '') ad_err('Missing X-Ad-Key header.', 401); if (!hash_equals(ADS_ADMIN_KEY, $supplied)) ad_err('Invalid key.', 403); // ── Data helpers ────────────────────────────────────────────────────────────── function load_ads(): array { if (!file_exists(ADS_JSON_PATH)) return []; $raw = json_decode(file_get_contents(ADS_JSON_PATH), true); return is_array($raw) ? $raw : []; } function save_ads(array $ads): void { $json = json_encode(array_values($ads), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); $tmp = ADS_JSON_PATH . '.tmp.' . getmypid(); if (file_put_contents($tmp, $json, LOCK_EX) === false) ad_err('Write failed.', 500); if (!rename($tmp, ADS_JSON_PATH)) { @unlink($tmp); ad_err('Rename failed.', 500); } } function find_ad(array $ads, string $id): int|false { foreach ($ads as $i => $a) { if (($a['id'] ?? '') === $id) return $i; } return false; } function new_id(array $ads): string { $existing = array_column($ads, 'id'); do { $id = 'ad_' . strtolower(bin2hex(random_bytes(4))); } while (in_array($id, $existing)); return $id; } function read_body(): array { $raw = file_get_contents('php://input'); if (!$raw) return []; $d = json_decode($raw, true); if (!is_array($d)) ad_err('Body must be valid JSON.'); return $d; } function sanitise_ad(array $in, bool $require_fields = true): array { $errors = []; if ($require_fields) { if (trim($in['title'] ?? '') === '') $errors[] = '"title" is required.'; if (trim($in['placement'] ?? '') === '') $errors[] = '"placement" is required.'; $valid_placements = array_keys(unserialize(ADS_PLACEMENTS)); if (!in_array($in['placement'] ?? '', $valid_placements)) $errors[] = 'Invalid placement.'; } if ($errors) ad_err(implode(' ', $errors)); $out = []; if (isset($in['title'])) $out['title'] = trim((string)$in['title']); if (isset($in['placement'])) $out['placement'] = trim((string)$in['placement']); if (isset($in['click_url'])) $out['click_url'] = trim((string)$in['click_url']); if (isset($in['active'])) $out['active'] = (bool)$in['active']; if (isset($in['note'])) $out['note'] = trim((string)$in['note']); // sizes: array of {size_key, media_url, media_type, custom_w, custom_h} if (isset($in['sizes']) && is_array($in['sizes'])) { $clean_sizes = []; foreach ($in['sizes'] as $s) { if (empty($s['size_key']) || empty($s['media_url'])) continue; $clean_sizes[] = [ 'size_key' => trim((string)$s['size_key']), 'media_url' => trim((string)$s['media_url']), 'media_type' => in_array($s['media_type'] ?? '', ['image','video']) ? $s['media_type'] : 'image', 'custom_w' => isset($s['custom_w']) ? (int)$s['custom_w'] : null, 'custom_h' => isset($s['custom_h']) ? (int)$s['custom_h'] : null, ]; } $out['sizes'] = $clean_sizes; } return $out; } // ── Routing ─────────────────────────────────────────────────────────────────── $method = $_SERVER['REQUEST_METHOD']; $id = trim($_GET['id'] ?? ''); $action = trim($_GET['action'] ?? ''); // Toggle active if ($method === 'POST' && $action === 'toggle') { $body = read_body(); $tid = trim((string)($body['id'] ?? '')); $ads = load_ads(); $idx = find_ad($ads, $tid); if ($idx === false) ad_err("Ad \"$tid\" not found.", 404); $ads[$idx]['active'] = (bool)($body['active'] ?? !$ads[$idx]['active']); save_ads($ads); ad_ok($ads[$idx]); } // Reorder if ($method === 'POST' && $action === 'reorder') { $body = read_body(); if (!isset($body['ids']) || !is_array($body['ids'])) ad_err('"ids" required.'); $ads = load_ads(); $indexed = []; foreach ($ads as $a) $indexed[$a['id']] = $a; $reordered = []; foreach ($body['ids'] as $rid) { if (!isset($indexed[$rid])) ad_err("Unknown id \"$rid\"."); $reordered[] = $indexed[$rid]; unset($indexed[$rid]); } foreach ($indexed as $a) $reordered[] = $a; save_ads($reordered); ad_ok(['reordered' => count($reordered)]); } // Create if ($method === 'POST') { $body = read_body(); $clean = sanitise_ad($body, true); $ads = load_ads(); $ad = array_merge([ 'id' => new_id($ads), 'title' => '', 'placement' => 'above', 'click_url' => '', 'active' => true, 'note' => '', 'sizes' => [], 'created' => (new DateTime('now', new DateTimeZone('UTC')))->format(DateTime::ATOM), ], $clean); array_unshift($ads, $ad); save_ads($ads); ad_ok($ad, 201); } // Update if ($method === 'PUT') { if ($id === '') ad_err('?id= required for PUT.'); $ads = load_ads(); $idx = find_ad($ads, $id); if ($idx === false) ad_err("Ad \"$id\" not found.", 404); $body = read_body(); $clean = sanitise_ad($body, false); $ads[$idx] = array_merge($ads[$idx], $clean); $ads[$idx]['id'] = $id; save_ads($ads); ad_ok($ads[$idx]); } // Delete if ($method === 'DELETE') { if ($id === '') ad_err('?id= required for DELETE.'); $ads = load_ads(); $idx = find_ad($ads, $id); if ($idx === false) ad_err("Ad \"$id\" not found.", 404); $deleted = $ads[$idx]; array_splice($ads, $idx, 1); save_ads($ads); ad_ok($deleted); } ad_err('Method not allowed.', 405);