'Unauthenticated']); } $user = currentUser(); // Accept JSON or form data $input = []; $raw = file_get_contents('php://input'); if ($raw) { $input = json_decode($raw, true) ?? []; } else { $input = $_POST; } $action = $input['action'] ?? ''; // ── Helper: owns survey? ────────────────────────────────────── function getSurveyForUser(int $id, array $user): ?array { if (in_array($user['role'], ['super_admin', 'manager'])) { return DB::row("SELECT * FROM surveys WHERE id = ?", [$id]); } return DB::row("SELECT * FROM surveys WHERE id = ? AND user_id = ?", [$id, $user['id']]); } try { switch ($action) { // ── save_structure ──────────────────────────────────────── case 'save_structure': { try { $surveyId = (int)($input['survey_id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } $pages = $input['pages'] ?? []; $pageIds = []; $questionIds = []; $pageNum = 1; // Get current page/question IDs to know what to delete later $oldPageIds = array_column(DB::all("SELECT id FROM survey_pages WHERE survey_id=?", [$surveyId]), 'id'); foreach ($pages as $page) { $pid = (int)($page['id'] ?? 0); $pageTitle = substr(strip_tags($page['title'] ?? 'Page ' . $pageNum), 0, 255); $pageDesc = substr(strip_tags($page['description'] ?? ''), 0, 1000); if ($pid && in_array($pid, $oldPageIds)) { DB::query("UPDATE survey_pages SET page_number=?, title=?, description=?, updated_at=NOW() WHERE id=? AND survey_id=?", [$pageNum, $pageTitle, $pageDesc, $pid, $surveyId]); } else { $pid = DB::insert("INSERT INTO survey_pages (survey_id, page_number, title, description) VALUES (?,?,?,?)", [$surveyId, $pageNum, $pageTitle, $pageDesc]); } $pageIds[] = $pid; // Questions $oldQIds = array_column(DB::all("SELECT id FROM survey_questions WHERE page_id=?", [$pid]), 'id'); $incomingQIds = []; $orderNum = 1; foreach (($page['questions'] ?? []) as $q) { $qid = (int)($q['id'] ?? 0); $qType = preg_replace('/[^a-z_]/', '', $q['type'] ?? 'text_short'); $qTitle = substr(strip_tags($q['title'] ?? 'Question'), 0, 500); $qDesc = substr(strip_tags($q['description'] ?? ''), 0, 2000); $qRequired = !empty($q['required']) ? 1 : 0; $qOptions = json_encode($q['options'] ?? null); // Merge media stimulus fields into settings JSON $qSettingsArr = $q['settings'] ?? []; if (!is_array($qSettingsArr)) $qSettingsArr = []; if (isset($q['media_url'])) $qSettingsArr['media_url'] = filter_var($q['media_url'], FILTER_SANITIZE_URL); if (isset($q['media_type'])) $qSettingsArr['media_type'] = in_array($q['media_type'], ['none','image','video','image_upload']) ? $q['media_type'] : 'none'; if (isset($q['media_position'])) $qSettingsArr['media_position'] = in_array($q['media_position'], ['above','below']) ? $q['media_position'] : 'above'; $qSettings = json_encode($qSettingsArr ?: null); $qLogic = json_encode($q['logic'] ?? []); $qPipe = json_encode($q['pipe_config'] ?? null); // Store screenout_url inside pipe_config or as part of logic JSON if (!empty($q['screenout_url'])) { $logicArr = $q['logic'] ?? []; $logicArr['screenout_url'] = filter_var($q['screenout_url'], FILTER_SANITIZE_URL); $qLogic = json_encode($logicArr); } $qLoop = json_encode($q['loop_config'] ?? null); $qLogicMatch = in_array($q['logic_match'] ?? 'all', ['all','any']) ? ($q['logic_match'] ?? 'all') : 'all'; $qLogicAct = in_array($q['logic_action'] ?? 'show', ['show','skip','jump_page','end','disqualify']) ? ($q['logic_action'] ?? 'show') : 'show'; if ($qid && in_array($qid, $oldQIds)) { DB::query("UPDATE survey_questions SET page_id=?, survey_id=?, type=?, title=?, description=?, required=?, options=?, settings=?, logic=?, logic_match=?, logic_action=?, pipe_config=?, loop_config=?, order_num=?, updated_at=NOW() WHERE id=?", [$pid, $surveyId, $qType, $qTitle, $qDesc, $qRequired, $qOptions, $qSettings, $qLogic, $qLogicMatch, $qLogicAct, $qPipe, $qLoop, $orderNum, $qid]); } else { $qid = DB::insert("INSERT INTO survey_questions (survey_id, page_id, type, title, description, required, options, settings, logic, logic_match, logic_action, pipe_config, loop_config, order_num) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [$surveyId, $pid, $qType, $qTitle, $qDesc, $qRequired, $qOptions, $qSettings, $qLogic, $qLogicMatch, $qLogicAct, $qPipe, $qLoop, $orderNum]); } $incomingQIds[] = $qid; $questionIds[] = $qid; $orderNum++; } // Delete removed questions $toDeleteQ = array_diff($oldQIds, $incomingQIds); foreach ($toDeleteQ as $dqid) { DB::query("DELETE FROM survey_questions WHERE id=?", [$dqid]); } $pageNum++; } // Delete removed pages $toDeleteP = array_diff($oldPageIds, $pageIds); foreach ($toDeleteP as $dpid) { DB::query("DELETE FROM survey_questions WHERE page_id=?", [$dpid]); DB::query("DELETE FROM survey_pages WHERE id=?", [$dpid]); } DB::query("UPDATE surveys SET updated_at=NOW() WHERE id=?", [$surveyId]); sendJson(['success' => true, 'page_ids' => $pageIds, 'question_ids' => $questionIds]); } catch (Exception $e) { echo json_encode(['error' => 'Save error: ' . $e->getMessage()]); } break; } // ── update_status ───────────────────────────────────────── case 'update_status': { $surveyId = (int)($input['id'] ?? 0); $status = $input['status'] ?? ''; $allowed = ['draft', 'active', 'paused', 'closed', 'archived']; if (!in_array($status, $allowed)) { echo json_encode(['error' => 'Invalid status']); exit; } $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } DB::query("UPDATE surveys SET status=?, updated_at=NOW() WHERE id=?", [$status, $surveyId]); sendJson(['success' => true]); break; } // ── update_settings ─────────────────────────────────────── case 'update_settings': { $surveyId = (int)($input['survey_id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } $title = substr(strip_tags($input['title'] ?? $survey['title']), 0, 255); $desc = substr(strip_tags($input['description'] ?? ''), 0, 2000); $startsAt = !empty($input['starts_at']) ? date('Y-m-d H:i:s', strtotime($input['starts_at'])) : null; $closesAt = !empty($input['closes_at']) ? date('Y-m-d H:i:s', strtotime($input['closes_at'])) : null; $settings = [ 'one_response_per_ip' => !empty($input['one_response_per_ip']), 'show_progress_bar' => !empty($input['show_progress_bar']), 'randomize_pages' => !empty($input['randomize_pages']), 'allow_back' => !empty($input['allow_back']), 'save_partial' => !empty($input['save_partial']), 'anonymous' => !empty($input['anonymous']), 'thank_you_message' => substr(strip_tags($input['thank_you_message'] ?? 'Thank you for completing this survey!'), 0, 500), 'redirect_url' => filter_var($input['redirect_url'] ?? '', FILTER_SANITIZE_URL), 'response_limit' => max(0, (int)($input['response_limit'] ?? 0)), ]; DB::query("UPDATE surveys SET title=?, description=?, settings=?, starts_at=?, closes_at=?, updated_at=NOW() WHERE id=?", [$title, $desc, json_encode($settings), $startsAt, $closesAt, $surveyId]); sendJson(['success' => true, 'message' => 'Settings saved']); break; } // ── update_theme ────────────────────────────────────────── case 'update_theme': { $surveyId = (int)($input['survey_id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } // Admins always allowed; clients need Starter+ for remove_branding $isAdmin = in_array($user['role'], ['super_admin','manager']); $u = DB::row("SELECT p.custom_branding FROM users u JOIN plans p ON u.plan_id=p.id WHERE u.id=?", [$user['id']]); $hasBranding = $isAdmin || !empty($u['custom_branding']); $theme = [ 'primary_color' => preg_match('/^#[0-9a-fA-F]{6}$/', $input['primary_color'] ?? '') ? $input['primary_color'] : '#e94560', 'bg_color' => preg_match('/^#[0-9a-fA-F]{6}$/', $input['bg_color'] ?? '') ? $input['bg_color'] : '#f4f6fb', 'logo_url' => filter_var($input['logo_url'] ?? '', FILTER_SANITIZE_URL), 'remove_branding' => $hasBranding && !empty($input['remove_branding']), ]; DB::query("UPDATE surveys SET theme=?, updated_at=NOW() WHERE id=?", [json_encode($theme), $surveyId]); sendJson(['success' => true, 'message' => 'Theme saved']); break; } // ── get_quotas ──────────────────────────────────────────── case 'get_quotas': { $surveyId = (int)($input['survey_id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } $quotas = DB::all("SELECT * FROM survey_quotas WHERE survey_id=? ORDER BY id", [$surveyId]); echo json_encode(['success' => true, 'quotas' => $quotas]); break; } // ── add_quota ───────────────────────────────────────────── case 'add_quota': { $surveyId = (int)($input['survey_id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } $name = substr(strip_tags($input['name'] ?? ''), 0, 255); $limit = max(1, (int)($input['limit_count'] ?? 1)); $action = in_array($input['action'] ?? 'disqualify', ['disqualify','complete','redirect']) ? $input['action'] : 'disqualify'; $conditions = json_encode($input['conditions'] ?? []); if (!$name) { echo json_encode(['error' => 'Quota name is required']); exit; } $id = DB::insert("INSERT INTO survey_quotas (survey_id, name, limit_count, action_when_full, conditions) VALUES (?,?,?,?,?)", [$surveyId, $name, $limit, $action, $conditions]); echo json_encode(['success' => true, 'id' => $id]); break; } // ── delete_quota ────────────────────────────────────────── case 'delete_quota': { $quotaId = (int)($input['id'] ?? 0); $quota = DB::row("SELECT sq.*, s.user_id FROM survey_quotas sq JOIN surveys s ON sq.survey_id=s.id WHERE sq.id=?", [$quotaId]); if (!$quota) { echo json_encode(['error' => 'Not found']); exit; } if ($quota['user_id'] != $user['id'] && !in_array($user['role'], ['super_admin','manager'])) { echo json_encode(['error' => 'Forbidden']); exit; } DB::query("DELETE FROM survey_quotas WHERE id=?", [$quotaId]); echo json_encode(['success' => true]); break; } // ── duplicate survey ────────────────────────────────────── case 'duplicate': { $surveyId = (int)($input['id'] ?? 0); $survey = getSurveyForUser($surveyId, $user); if (!$survey) { echo json_encode(['error' => 'Not found']); exit; } if (!canCreateSurvey()) { echo json_encode(['error' => 'Survey limit reached. Upgrade your plan.']); exit; } $token = generateToken(32); $newTitle = 'Copy of ' . $survey['title']; $newId = DB::insert("INSERT INTO surveys (user_id, title, description, token, settings, theme) VALUES (?,?,?,?,?,?)", [$user['id'], $newTitle, $survey['description'], $token, $survey['settings'], $survey['theme']]); $pages = DB::all("SELECT * FROM survey_pages WHERE survey_id=? ORDER BY page_number", [$surveyId]); foreach ($pages as $page) { $newPageId = DB::insert("INSERT INTO survey_pages (survey_id, page_number, title, description) VALUES (?,?,?,?)", [$newId, $page['page_number'], $page['title'], $page['description']]); $questions = DB::all("SELECT * FROM survey_questions WHERE page_id=? ORDER BY order_num", [$page['id']]); foreach ($questions as $q) { DB::insert("INSERT INTO survey_questions (survey_id, page_id, type, title, description, required, options, settings, logic, order_num) VALUES (?,?,?,?,?,?,?,?,?,?)", [$newId, $newPageId, $q['type'], $q['title'], $q['description'], $q['required'], $q['options'], $q['settings'], '[]', $q['order_num']]); } } DB::query("UPDATE users SET surveys_created=surveys_created+1 WHERE id=?", [$user['id']]); echo json_encode(['success' => true, 'new_id' => $newId, 'redirect' => APP_URL . '/surveys/builder.php?id=' . $newId]); break; } default: ob_clean(); echo json_encode(['error' => 'Unknown action']); } } catch (Throwable $e) { sendJson(['error' => 'Server error: ' . $e->getMessage()]); }