[ 'title' => 'Bestand und Technik', 'questions' => [ '1.1' => 'In welcher Sprache und mit welchem Framework ist der Telegram-Bot gebaut?', '1.2' => 'Wo läuft der Bot (Hoster, Server, Container)? Gibt es eine Staging- oder Testumgebung?', '1.3' => 'Welche Datenbank wird genutzt, und was wird dort gespeichert?', '1.4' => 'Welche Unterlagen gibt es, in die wir Einblick bekommen?', '1.5' => 'Wie kommen die Handelssignale zustande, und wer steuert sie?', '1.6' => 'Wer ist für Betrieb und Wartung zuständig, und bleibt das so?', ], ], '2' => [ 'title' => 'Nutzer und Umfang', 'questions' => [ '2.1' => 'Wie viele aktive Nutzer gibt es heute, und wie viele erwarten Sie nach 12 Monaten?', '2.2' => 'Wie viele Orders laufen pro Tag durch das System, und wie hoch ist die erwartete Spitzenlast?', '2.3' => 'Sollen bestehende Telegram-Nutzer übernommen werden? Wenn ja, wie?', '2.4' => 'Wird der Telegram-Bot nach dem Start abgeschaltet oder parallel weiterbetrieben?', '2.5' => 'Welche Sprachen soll die App unterstützen?', ], ], '3' => [ 'title' => 'Provisionsmodell', 'questions' => [ '3.1' => 'Worauf wird die Provision berechnet?', '3.2' => 'Wie hoch sind die Sätze?', '3.3' => 'Gibt es nur direkte Empfehlungen oder mehrere Ebenen?', '3.4' => 'Wie werden Verluste, stornierte oder fehlgeschlagene Orders behandelt? Gibt es Rückforderungen?', '3.5' => 'Wann gilt eine Provision als verdient, und wann ist sie auszahlbar?', '3.6' => 'Wie erfolgt die Auszahlung, und wer führt sie durch?', '3.7' => 'Welche Nachweise braucht die Buchhaltung?', ], ], '4' => [ 'title' => 'Referral und Missbrauchsschutz', 'questions' => [ '4.1' => 'Wie sieht ein Referral aus?', '4.2' => 'Darf ein Nutzer den Referrer nachträglich ändern?', '4.3' => 'Welche Missbrauchsfälle sind Ihnen begegnet oder befürchten Sie?', '4.4' => 'Was soll bei Verdacht passieren?', '4.5' => 'Darf ein Kraken-Account nur genau einem App-Nutzer zugeordnet sein?', '4.6' => 'Welche Verifizierung muss ein geworbener Nutzer durchlaufen, bevor der Referrer eine Provision erhält?', ], ], '5' => [ 'title' => 'Rollen und Admin-Bereich', 'questions' => [ '5.1' => 'Welche Rollen gibt es?', '5.2' => 'Was muss der Admin-Bereich können?', '5.3' => 'Wer bekommt Admin-Zugriff, und sollen Admin-Aktionen protokolliert werden?', ], ], '6' => [ 'title' => 'Kraken und Sicherheit', 'questions' => [ '6.1' => 'Werden ausschließlich Kraken-Konten angebunden, oder sind weitere Börsen geplant?', '6.2' => 'Wie werden die API-Keys der Nutzer heute gespeichert und geschützt?', '6.3' => 'Welche Limits gelten pro Nutzer, und soll es einen Not-Aus geben?', '6.4' => 'Welche Login-Anforderungen gelten?', '6.5' => 'Welche Benachrichtigungen soll die App senden?', ], ], '7' => [ 'title' => 'Recht und Store-Veröffentlichung', 'questions' => [ '7.1' => 'Können Sie uns die vorliegenden Erlaubnisse bzw. das Rechtsgutachten für den Store-Review zur Verfügung stellen?', '7.2' => 'Wer ist Herausgeber der App, und gibt es Entwicklerkonten bei Apple und Google?', '7.3' => 'Welche Texte liegen bereits vor?', ], ], '8' => [ 'title' => 'Design', 'questions' => [ '8.1' => 'Liegt das Figma-Design vollständig vor?', '8.2' => 'Gibt es Marken-Vorgaben wie Logo, Farben und lizenzierte Schriften?', ], ], '9' => [ 'title' => 'Rahmen', 'questions' => [ '9.1' => 'Wann sollen Start und Livegang sein? Gibt es harte Fristen, etwa eine Kampagne oder ein Event?', '9.2' => 'In welchem Budgetrahmen bewegen Sie sich?', '9.3' => 'Wer ist Ansprechpartner mit Entscheidungsbefugnis, und wie schnell sind Freigaben möglich?', '9.4' => 'Was gehört zwingend in die erste Version, und was kann später folgen?', '9.5' => 'Wie soll der Betrieb nach dem Start laufen?', ], ], ]; function out(int $status, array $payload): never { http_response_code($status); echo json_encode($payload, JSON_UNESCAPED_UNICODE); exit; } function fail(int $status, string $message): never { out($status, ['ok' => false, 'error' => $message]); } function clean(string $s, int $max): string { $s = str_replace("\0", '', $s); $s = preg_replace('/[^\P{C}\n\t]+/u', '', $s) ?? ''; return mb_substr(trim($s), 0, $max); } header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { header('Allow: POST'); fail(405, 'Nur POST erlaubt.'); } if (stripos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') === false) { fail(415, 'Ungültiges Format.'); } $raw = file_get_contents('php://input', false, null, 0, MAX_BODY + 1); if ($raw === false || $raw === '') { fail(400, 'Leere Anfrage.'); } if (strlen($raw) > MAX_BODY) { fail(413, 'Die Antworten sind zu umfangreich.'); } $data = json_decode($raw, true); if (!is_array($data)) { fail(400, 'Ungültige Daten.'); } /* ---- Spam-Schutz ---- */ if (($data['website'] ?? '') !== '') { // Honeypot gefüllt: so tun, als wäre alles gut, aber nichts verarbeiten. out(200, ['ok' => true]); } $startedAt = $data['startedAt'] ?? 0; if (!is_int($startedAt) && !is_float($startedAt)) { fail(400, 'Ungültige Daten.'); } $elapsed = time() - (int)floor($startedAt / 1000); if ($elapsed < MIN_SECONDS) { fail(429, 'Das ging zu schnell. Bitte versuchen Sie es in einem Moment erneut.'); } /* ---- Eingaben prüfen ---- */ $name = clean((string)($data['name'] ?? ''), 120); if ($name === '') { fail(422, 'Bitte tragen Sie Ihren Namen und Ihr Unternehmen ein.'); } $email = clean((string)($data['email'] ?? ''), 200); if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) { fail(422, 'Die E-Mail-Adresse ist ungültig.'); } $known = []; foreach (CHAPTERS as $chapter) { foreach ($chapter['questions'] as $id => $text) { $known[$id] = true; } } $answers = $data['answers'] ?? []; if (!is_array($answers)) { fail(400, 'Ungültige Daten.'); } $clean = []; foreach ($answers as $id => $a) { $id = (string)$id; if (!isset($known[$id]) || !is_array($a)) { fail(400, 'Ungültige Daten.'); } $chips = []; foreach ((array)($a['c'] ?? []) as $c) { if (!is_string($c)) { fail(400, 'Ungültige Daten.'); } $chips[] = clean($c, 200); if (count($chips) >= 10) { break; } } $clean[$id] = [ 'c' => $chips, 't' => clean((string)($a['t'] ?? ''), 5000), 'o' => ($a['o'] ?? false) === true, ]; } /* ---- Rate-Limit pro IP (nur Hash gespeichert) ---- */ if (!is_dir(DATA_DIR) && !@mkdir(DATA_DIR, 0750, true) && !is_dir(DATA_DIR)) { error_log('submit.php: DATA_DIR kann nicht angelegt werden: ' . DATA_DIR); fail(500, 'Die Antworten konnten nicht gespeichert werden.'); } @file_put_contents(DATA_DIR . '/.htaccess', "Require all denied\nDeny from all\n"); @file_put_contents(DATA_DIR . '/index.html', ''); $rlDir = DATA_DIR . '/ratelimit'; @mkdir($rlDir, 0750, true); $ipHash = hash('sha256', ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '|' . __FILE__); $rlFile = $rlDir . '/' . $ipHash . '.json'; $now = time(); $hits = []; if (is_file($rlFile)) { $prev = json_decode((string)@file_get_contents($rlFile), true); if (is_array($prev)) { foreach ($prev as $t) { if (is_int($t) && $t > $now - 3600) { $hits[] = $t; } } } } if (count($hits) >= MAX_PER_HOUR) { fail(429, 'Zu viele Einsendungen. Bitte versuchen Sie es später erneut.'); } $hits[] = $now; @file_put_contents($rlFile, json_encode($hits), LOCK_EX); /* ---- Text erzeugen (serverseitig, dem Client-Text wird nicht vertraut) ---- */ $text = 'Anforderungen Trading-App: Antworten' . "\n"; $text .= 'Von: ' . $name . "\n"; if ($email !== '') { $text .= 'E-Mail: ' . $email . "\n"; } $text .= 'Eingegangen: ' . date('d.m.Y H:i') . "\n\n"; foreach (CHAPTERS as $ci => $chapter) { $text .= $ci . '. ' . $chapter['title'] . "\n"; foreach ($chapter['questions'] as $id => $q) { $a = $clean[$id] ?? null; $parts = []; if ($a !== null) { if ($a['c']) { $parts[] = implode(', ', $a['c']); } if ($a['t'] !== '') { $parts[] = $a['t']; } if ($a['o']) { $parts[] = '(noch offen)'; } } $text .= $id . ' ' . $q . "\n" . 'Antwort: ' . ($parts ? implode(' | ', $parts) : 'nicht beantwortet') . "\n\n"; } } /* ---- Ablegen ---- */ $base = DATA_DIR . '/' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)); $json = json_encode( ['name' => $name, 'email' => $email, 'received' => date('c'), 'answers' => $clean], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ); $saved = @file_put_contents($base . '.txt', $text, LOCK_EX) !== false && @file_put_contents($base . '.json', (string)$json, LOCK_EX) !== false; /* ---- Mail ---- */ $mailed = false; if (MAIL_ENABLED) { $subject = preg_replace('/[\r\n]+/', ' ', SUBJECT_BASE . ': ' . $name) ?? SUBJECT_BASE; $headers = [ 'From: ' . MAIL_FROM, 'MIME-Version: 1.0', 'Content-Type: text/plain; charset=UTF-8', 'Content-Transfer-Encoding: 8bit', ]; if ($email !== '') { $headers[] = 'Reply-To: ' . $email; // bereits per FILTER_VALIDATE_EMAIL geprüft } // Hier bei Bedarf auf SMTP (z. B. PHPMailer) umstellen, falls mail() schlecht zugestellt wird. $mailed = @mail(RECIPIENT, mb_encode_mimeheader($subject, 'UTF-8'), $text, implode("\r\n", $headers)); } if (!$saved && !$mailed) { error_log('submit.php: weder gespeichert noch gemailt (' . $base . ')'); fail(500, 'Die Antworten konnten nicht übermittelt werden.'); } if (!$mailed && MAIL_ENABLED) { error_log('submit.php: Mailversand fehlgeschlagen, Datei gespeichert: ' . $base . '.txt'); } out(200, ['ok' => true]);