0, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); session_name('WLISTSESSID'); session_start(); umask(0022); // sorgt für 0644/0755 als Default für neu erstellte Dateien/Ordner require_once __DIR__ . '/config/config.php'; // ===== Debug Toggle ===== if (!empty($app_debug)) { ini_set('display_errors', '1'); ini_set('display_startup_errors', '1'); ini_set('log_errors', '1'); if (!is_dir(__DIR__ . '/logs')) { @mkdir(__DIR__ . '/logs', 0750, true); } ini_set('error_log', __DIR__ . '/logs/php-error.log'); error_reporting(E_ALL); } else { ini_set('display_errors', '0'); ini_set('display_startup_errors', '0'); ini_set('log_errors', '1'); if (!is_dir(__DIR__ . '/logs')) { @mkdir(__DIR__ . '/logs', 0750, true); } ini_set('error_log', __DIR__ . '/logs/php-error.log'); error_reporting(E_ALL); } /* ============= Helpers ============= */ function fail(string $msg = 'Unerwarteter Fehler', int $code = 400): void { http_response_code($code); exit($msg); } function require_csrf(): void { $t = (string) ($_POST['csrf'] ?? ''); if (empty($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $t)) fail('Ungültiges CSRF-Token', 403); } function require_logged_in(): int { if (!isset($_SESSION['listid'])) fail('Nicht eingeloggt', 403); return (int) $_SESSION['listid']; } function db(): mysqli { global $servername, $username, $password, $db; $conn = new mysqli($servername, $username, $password, $db); if ($conn->connect_error) fail('Interner Fehler (DB)', 500); $conn->set_charset('utf8mb4'); return $conn; } function parse_price_to_cents(string $in): int { $clean = preg_replace('/[^\d,\.]/', '', $in ?? ''); $norm = str_replace('.', '', $clean); $norm = str_replace(',', '.', $norm); if ($norm === '' || !is_numeric($norm)) return 0; $cents = (int) round((float) $norm * 100); if ($cents < 0) $cents = 0; if ($cents > 100000000) $cents = 100000000; // 1 Mio € return $cents; } function is_valid_http_url(string $url): bool { if (!filter_var($url, FILTER_VALIDATE_URL)) return false; $p = parse_url($url); if (!$p || empty($p['scheme']) || empty($p['host'])) return false; $s = strtolower($p['scheme']); return $s === 'http' || $s === 'https'; } /* ============= Controller ============= */ require_csrf(); $sessionListId = require_logged_in(); $action = strtolower(trim((string) ($_POST['action'] ?? ''))); $ItemTitle = trim((string) ($_POST['ItemTitle'] ?? '')); $ItemDescription = trim((string) ($_POST['ItemDescription'] ?? '')); $ItemPrice = parse_price_to_cents((string) ($_POST['ItemPrice'] ?? '')); $ItemQty = isset($_POST['ItemQty']) ? max(1, (int) $_POST['ItemQty']) : 1; $ItemLink = trim((string) ($_POST['ItemLink'] ?? '')); $ItemImageUrl = trim((string) ($_POST['ItemImage'] ?? '')); // optional URL zum Pull $ListUUID = trim((string) ($_POST['ItemListUUID'] ?? '')); $sortbyTransfer = (string) ($_POST['sortby_transfer'] ?? 'priority'); $removeImage = isset($_POST['RemoveImage']) && ($_POST['RemoveImage'] === '1'); $WhishID = isset($_POST['WhishID']) ? (int) $_POST['WhishID'] : 0; // bei edit if (!preg_match('/^[0-9a-fA-F-]{32,36}$/', $ListUUID)) fail('Liste nicht autorisiert', 403); $conn = db(); /* UUID -> int ID */ $stmt = $conn->prepare('SELECT ID FROM lists WHERE uuid = ?'); $stmt->bind_param('s', $ListUUID); $stmt->execute(); $res = $stmt->get_result(); if (!$res || !($row = $res->fetch_assoc())) { $stmt->close(); $conn->close(); fail('Liste nicht autorisiert', 403); } $ListID = (int) $row['ID']; $stmt->close(); /* Session muss zu dieser Liste gehören */ if ($ListID !== $sessionListId) { $conn->close(); fail('Liste nicht autorisiert', 403); } /* Validierungen (add & edit) */ if ($ItemTitle === '') fail('Titel fehlt', 400); if ($ItemLink !== '' && !is_valid_http_url($ItemLink)) fail('Ungültiger Angebotslink', 400); /* Optional: Bild von externer URL holen */ $imageLocalLink = null; if (!$removeImage && $ItemImageUrl !== '') { $whitelist = $image_host_whitelist ?? null; $fetch = ImageFetch::download($ItemImageUrl, [ 'max_bytes' => 8_000_000, 'timeout' => 12, 'connect_timeout' => 5, 'retries' => 4, 'retry_backoff_ms' => 300, 'whitelist_hosts' => $whitelist, 'ip_resolve_v4' => true, 'referer' => 'auto', 'log_prefix' => 'wishlist-img', ]); if (!$fetch['ok']) { error_log("wishlist image error: http=" . ($fetch['http_code'] ?? 0) . " curl=" . ($fetch['curl_err'] ?? '-') . " url=$ItemImageUrl"); $conn->close(); fail('Bild-Download fehlgeschlagen', 400); } $info = @getimagesize($fetch['tmp_path']); $mime = $info['mime'] ?? $fetch['mime'] ?? 'image/*'; if (stripos($mime, 'image/') !== 0) { @unlink($fetch['tmp_path']); $conn->close(); fail('Link ist kein gültiges Bild', 400); } global $imagedir; if (!is_dir($imagedir)) @mkdir($imagedir, 0755, true); $filename = ImageFetch::safeFileNameFromUrl($ItemImageUrl); $target = rtrim($imagedir, '/') . '/' . $filename; if (!@rename($fetch['tmp_path'], $target)) { if (!@copy($fetch['tmp_path'], $target)) { @unlink($fetch['tmp_path']); $conn->close(); fail('Bildspeicherung fehlgeschlagen', 500); } @unlink($fetch['tmp_path']); } @chmod($target, 0644); $imageLocalLink = $filename; } /* ====== ADD ====== */ if ($action === 'add') { // nächste Priority ermitteln $next = 1; $s = $conn->prepare('SELECT COALESCE(MAX(priority),0) AS maxp FROM wishes WHERE wishlist = ?'); $s->bind_param('i', $ListID); $s->execute(); $r = $s->get_result(); if ($r && $m = $r->fetch_assoc()) $next = ((int) $m['maxp']) + 1; $s->close(); $stmt = $conn->prepare(' INSERT INTO wishes (title, description, link, image, price, wishlist, priority, qty) VALUES (?, ?, ?, ?, ?, ?, ?, ?) '); $link = $ItemLink !== '' ? $ItemLink : null; $image = $imageLocalLink !== null ? $imageLocalLink : null; $stmt->bind_param( 'ssssiiii', $ItemTitle, $ItemDescription, $link, $image, $ItemPrice, $ListID, $next, $ItemQty ); if (!$stmt->execute()) { $stmt->close(); $conn->close(); fail('Speichern fehlgeschlagen', 500); } $stmt->close(); } /* ====== EDIT ====== */ elseif ($action === 'edit') { if ($WhishID <= 0) { $conn->close(); fail('Ungültige Item-ID', 400); } // Besitz prüfen & altes Bild holen $stmt = $conn->prepare('SELECT image FROM wishes WHERE ID = ? AND wishlist = ?'); $stmt->bind_param('ii', $WhishID, $ListID); $stmt->execute(); $res = $stmt->get_result(); if (!$res || !($row = $res->fetch_assoc())) { $stmt->close(); $conn->close(); fail('Item nicht gefunden/autorisiert', 404); } $oldImage = (string) $row['image']; $stmt->close(); // Bild-Entscheidung: entfernen, ersetzen oder behalten $newImage = $oldImage; if ($removeImage) { if (!empty($oldImage)) { global $imagedir; $full = rtrim($imagedir, '/') . '/' . $oldImage; if (is_file($full)) @unlink($full); } $newImage = ''; } elseif ($imageLocalLink !== null) { if (!empty($oldImage)) { global $imagedir; $full = rtrim($imagedir, '/') . '/' . $oldImage; if (is_file($full)) @unlink($full); } $newImage = $imageLocalLink; } $link = $ItemLink !== '' ? $ItemLink : null; $imgNullable = ($newImage !== '') ? $newImage : null; $stmt = $conn->prepare( 'UPDATE wishes SET title=?, description=?, link=?, image=?, price=?, qty=? WHERE ID=? AND wishlist=?' ); $stmt->bind_param( 'sssssiii', $ItemTitle, $ItemDescription, $link, $imgNullable, $ItemPrice, $ItemQty, $WhishID, $ListID ); if (!$stmt->execute()) { $stmt->close(); $conn->close(); fail('Update fehlgeschlagen', 500); } $stmt->close(); } /* ====== Unbekannt ====== */ else { $conn->close(); fail('Unbekannte Aktion', 400); } $conn->close(); /* Redirect zurück (sicher, mit UUID) */ $redirect = (string) ($_SERVER['HTTP_REFERER'] ?? ''); if ($redirect === '' || stripos($redirect, 'http') !== 0) { $host = $_SERVER['HTTP_HOST'] ?? ''; $scheme = $secure ? 'https' : 'http'; $qs = '?list=' . urlencode($ListUUID); if ($sortbyTransfer !== '') $qs .= '&sort=' . urlencode($sortbyTransfer); $redirect = $scheme . '://' . $host . '/' . $qs; } header('Location: ' . $redirect, true, 303); exit;