Files
Simple-Wishlist/index.php

583 lines
23 KiB
PHP

<?php
declare(strict_types=1);
// ===== Session Setup (einheitlich in allen Entry Points) =====
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_name('WLISTSESSID');
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/include/listgenerator.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);
}
$message = null;
if (!empty($_SESSION['flash']) && is_array($_SESSION['flash'])) {
$message = $_SESSION['flash']; // ['msg'=>..., 'type'=> success|warning|danger]
unset($_SESSION['flash']);
}
// ===== Helpers =====
function e(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function db(): mysqli
{
global $servername, $username, $password, $db;
$m = new mysqli($servername, $username, $password, $db);
if ($m->connect_error) {
http_response_code(500);
exit('Interner Fehler (DB)');
}
$m->set_charset('utf8mb4');
return $m;
}
function ensure_csrf_token(): void
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
}
function require_csrf(): void
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ok = isset($_POST['csrf']) && hash_equals($_SESSION['csrf'] ?? '', (string) $_POST['csrf']);
if (!$ok) {
http_response_code(403);
exit('Ungültiges CSRF-Token');
}
}
}
ensure_csrf_token();
// ===== URL-Param: Liste per UUID (oder Alt-ID -> Redirect) =====
$ListID = -1;
$ListUUID = '';
$showCreateEmptyState = false;
if (!isset($_GET['list'])) {
// Kein Parameter => nur Empty-State ohne Message
$showCreateEmptyState = true;
} else {
$raw = trim((string) $_GET['list']);
if ($raw === '') {
// Leerer Parameter => nur Empty-State ohne Message
$showCreateEmptyState = true;
} elseif (preg_match('/^[0-9a-fA-F-]{32,36}$/', $raw)) {
// UUID übergeben
$c = db();
$s = $c->prepare('SELECT ID, uuid FROM lists WHERE uuid=?');
$s->bind_param('s', $raw);
$s->execute();
$r = $s->get_result();
if ($r && ($row = $r->fetch_assoc())) {
$ListID = (int) $row['ID'];
$ListUUID = (string) $row['uuid'];
} else {
// UUID-Format ok, aber nicht vorhanden => Message + Empty-State
$message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
$showCreateEmptyState = true;
}
$s->close();
$c->close();
} elseif (preg_match('/^\d+$/', $raw)) {
// Numerische ID übergeben -> auf UUID umleiten, wenn vorhanden
$id = (int) $raw;
$c = db();
$s = $c->prepare('SELECT uuid FROM lists WHERE ID=?');
$s->bind_param('i', $id);
$s->execute();
$r = $s->get_result();
if ($r && ($row = $r->fetch_assoc())) {
$uuid = (string) $row['uuid'];
$scheme = $secure ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? '';
header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($uuid), true, 301);
exit;
} else {
// ID-Format ok, aber nicht vorhanden => Message + Empty-State
$message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
$showCreateEmptyState = true;
}
$s->close();
$c->close();
} else {
// Weder gültige UUID noch Zahl => Message + Empty-State
$message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
$showCreateEmptyState = true;
}
}
// ===== Sortierung (Whitelist) =====
$sortby = 'priority';
if (isset($_POST['sortby']))
$sortby = (string) $_POST['sortby'];
elseif (isset($_POST['sortby_transfer']))
$sortby = (string) $_POST['sortby_transfer'];
$allowedOrder = ['priority' => 'priority DESC', 'price_asc' => 'price ASC', 'price_desc' => 'price DESC', 'date_desc' => 'date DESC', 'date_asc' => 'date ASC', 'random' => 'RAND()'];
if (!array_key_exists($sortby, $allowedOrder))
$sortby = 'priority';
// ===== Login-Status =====
$loggedin = (isset($_SESSION['listid']) && $ListID === (int) $_SESSION['listid']);
$GLOBALS['loggedin'] = $loggedin; // für listgenerator.php
// ===== POST-Actions (mit CSRF) =====
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
// LOGIN
if (isset($_POST['login'])) {
$ListPassword = (string) ($_POST['ListPassword'] ?? '');
$ListIdFromForm = (int) ($_POST['ListID'] ?? -1);
$c = db();
$s = $c->prepare('SELECT edit_pw, uuid FROM lists WHERE ID=?');
$s->bind_param('i', $ListIdFromForm);
$s->execute();
$r = $s->get_result();
if ($r && ($row = $r->fetch_assoc())) {
if (password_verify($ListPassword, (string) $row['edit_pw'])) {
$_SESSION['listid'] = $ListIdFromForm;
$loggedin = true;
$ListUUID = (string) $row['uuid'];
$message = ['msg' => 'Login erfolgreich', 'type' => 'success'];
} else
$message = ['msg' => 'Falsches Passwort', 'type' => 'warning'];
} else
$message = ['msg' => 'Liste nicht gefunden', 'type' => 'warning'];
$s->close();
$c->close();
}
// LISTE ANLEGEN
if (isset($_POST['listadd'])) {
$listName = (string) ($_POST['listName'] ?? '');
$listPasswordRaw = (string) ($_POST['listPassword'] ?? '');
$listDescription = (string) ($_POST['listDescription'] ?? '');
$listPassword = password_hash($listPasswordRaw, PASSWORD_DEFAULT);
$c = db();
$s = $c->prepare('INSERT INTO lists (uuid, title, description, edit_pw) VALUES (UUID(), ?, ?, ?)');
$s->bind_param('sss', $listName, $listDescription, $listPassword);
if ($s->execute()) {
$last_id = $c->insert_id;
$g = $c->prepare('SELECT uuid FROM lists WHERE ID=?');
$g->bind_param('i', $last_id);
$g->execute();
$gr = $g->get_result();
$uuid = ($gr && ($row = $gr->fetch_assoc())) ? (string) $row['uuid'] : '';
$g->close();
$_SESSION['listid'] = $last_id;
$loggedin = true;
$scheme = $secure ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? '';
header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($uuid));
exit;
} else {
$message = ['msg' => 'Unerwarteter Fehler beim Anlegen', 'type' => 'danger'];
}
$s->close();
$c->close();
}
// LOGOUT
if (isset($_POST['logout'])) {
session_destroy();
$loggedin = false;
$message = ['msg' => 'Logout erfolgreich', 'type' => 'success'];
}
// PRIORITÄT PUSHEN
if (isset($_POST['pushprio'])) {
$wishId = (int) ($_POST['WhishID'] ?? -1);
$c = db();
$s = $c->prepare('SELECT COALESCE(MAX(priority),0) AS maxprio FROM wishes WHERE wishlist=?');
$s->bind_param('i', $ListID);
$s->execute();
$r = $s->get_result();
$next = 1;
if ($r && ($row = $r->fetch_assoc()))
$next = ((int) $row['maxprio']) + 1;
$s->close();
$u = $c->prepare('UPDATE wishes SET priority=? WHERE ID=?');
$u->bind_param('ii', $next, $wishId);
$message = $u->execute() ? ['msg' => 'Wunschpriorität aktualisiert', 'type' => 'success'] : ['msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger'];
$u->close();
$c->close();
}
// LÖSCHEN (nur eingeloggt)
if (isset($_POST['delete']) && $loggedin === true) {
$WhishID = (int) ($_POST['WhishID'] ?? -1);
$WhishTitle = '';
$c = db();
$s = $c->prepare('SELECT image, title FROM wishes WHERE ID=?');
$s->bind_param('i', $WhishID);
$s->execute();
$r = $s->get_result();
if ($r && ($row = $r->fetch_assoc())) {
$WhishTitle = (string) $row['title'];
$imageFile = (string) $row['image'];
if (!empty($imageFile)) {
global $imagedir;
$full = rtrim($imagedir, '/') . '/' . $imageFile;
if (is_file($full))
@unlink($full);
}
}
$s->close();
$d = $c->prepare('DELETE FROM wishes WHERE ID=?');
$d->bind_param('i', $WhishID);
$message = $d->execute() ? ['msg' => 'Wunsch <b>"' . e($WhishTitle) . '"</b> gelöscht', 'type' => 'success'] : ['msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger'];
$d->close();
$c->close();
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Simple Wishlist</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/tweaks.css">
<link rel="stylesheet" href="css/fontawesome.min.css" />
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
<link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="img/favicon.ico">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2 bi bi-gift"
viewBox="0 0 16 16">
<path
d="M3 2.5a2.5 2.5 0 0 1 5 0 2.5 2.5 0 0 1 5 0v.006c0 .07 0 .27-.038.494H15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 14.5V7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.038A2.968 2.968 0 0 1 3 2.506V2.5zm1.068.5H7v-.5a1.5 1.5 0 1 0-3 0c0 .085.002.274.045.43a.522.522 0 0 0 .023.07zM9 3h2.932a.56.56 0 0 0 .023-.07c.043-.156.045-.345.045-.43a1.5 1.5 0 0 0-3 0V3zM1 4v2h6V4H1zm8 0v2h6V4H9zm5 3H9v8h4.5a.5.5 0 0 0 .5-.5V7zm-7 8V7H2v7.5a.5.5 0 0 0 .5.5H7z" />
</svg>
<strong>Simple Wishlist</strong>
</a>
<div class="nav navbar-nav navbar-right">
<div class="d-grid gap-2 d-flex">
<button type="button" class="btn btn-outline-success my-2 my-sm-0" data-bs-toggle="modal"
data-bs-target="#createListModal">Neue Liste</button>
<?php if ($showCreateEmptyState == false): ?>
<?php if ($loggedin): ?>
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
data-bs-target="#itemModal" data-mode="add" data-listuuid="<?= e($ListUUID) ?>"
data-sort="<?= e($sortby) ?>">Add Item</button>
<form class="form-inline" action="" method="POST">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="submit" class="btn btn-outline-secondary my-2 my-sm-0" name="logout">Logout</button>
</form>
<?php else: ?>
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
data-bs-target="#loginModal">Login</button>
<?php endif; ?>
<form class="form-inline" action="" method="POST">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<select class="form-control" name="sortby" id="sortby">
<option <?= $sortby === 'priority' ? 'selected' : '' ?> value="priority">Priorität</option>
<option <?= $sortby === 'price_asc' ? 'selected' : '' ?> value="price_asc">Preis aufsteigend</option>
<option <?= $sortby === 'price_desc' ? 'selected' : '' ?> value="price_desc">Preis absteigend</option>
<option <?= $sortby === 'date_desc' ? 'selected' : '' ?> value="date_desc">Datum, neu → alt</option>
<option <?= $sortby === 'date_asc' ? 'selected' : '' ?> value="date_asc">Datum, alt → neu</option>
<option <?= $sortby === 'random' ? 'selected' : '' ?> value="random">Zufall</option>
</select>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</header>
<main class="pb-4">
<?php if (isset($message)): ?>
<div class="alert alert-<?= e($message['type']) ?> alert-dismissible fade show m-3" role="alert">
<?= $message['msg'] ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (!empty($showCreateEmptyState)): ?>
<!-- Empty State -->
<section class="py-5 text-center container">
<div class="row py-lg-4">
<div class="col-lg-8 col-md-10 mx-auto">
<h1 class="fw-light">Willkommen bei Simple Wishlist</h1>
<p class="lead text-muted">
Es wurde keine gültige Liste ausgewählt. Du kannst jetzt eine neue Liste anlegen.
</p>
<p>
<button class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#createListModal">
Neue Liste anlegen
</button>
</p>
</div>
</div>
</section>
<?php else: ?>
<?php wishlistMainBuilder($ListID, $sortby); ?>
<?php endif; ?>
</main>
<footer class="mt-5 text-center text-muted small">
<div class="container">
<hr class="mb-3 opacity-25">
<p class="mb-1">
Made with
<img src="img/heart-icon.png" alt="Shakaru" class="icon-sm mx-1">
and
<img src="img/paw-icon.png" alt="Paw" class="icon-sm mx-1">
by
<strong>
<img src="img/shakaru-icon.png" alt="Shakaru" class="icon-sm mx-1">
Shakaru
</strong>
</p>
<p class="mb-0">
&copy; <?= date('Y') ?> Simple Wishlist · Marcel Peterkau
· <a href="#" class="text-decoration-none">Back to top ↑</a>
</p>
</div>
</footer>
<?php if (!$loggedin): ?>
<!-- Login Modal -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Login</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<label for="ListPassword" class="form-label">Passwort</label>
<input type="password" class="form-control" id="ListPassword" name="ListPassword" required>
<input type="hidden" id="ListID" name="ListID" value="<?= (int) $ListID ?>">
</div>
<div class="modal-footer">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" name="login" class="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Add/Edit Modal (ein Modal für beides) -->
<div class="modal fade" id="itemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="itemModalTitle">Wunsch hinzufügen</h5><button type="button" class="btn-close"
data-bs-dismiss="modal"></button>
</div>
<form action="item.php" method="POST" id="itemForm">
<div class="modal-body">
<label class="form-label">Titel</label>
<input class="form-control" name="ItemTitle" id="ItemTitle" required>
<label class="form-label mt-2">Beschreibung</label>
<textarea class="form-control" name="ItemDescription" id="ItemDescription" rows="3"></textarea>
<label class="form-label mt-2">Preis</label>
<div class="input-group">
<input class="form-control" name="ItemPrice" id="ItemPrice" placeholder="0,00">
<span class="input-group-text">€</span>
</div>
<label class="form-label mt-2">Anzahl</label>
<input class="form-control" name="ItemQty" id="ItemQty" type="number" min="1" step="1" value="1" required>
<label class="form-label mt-2">Link zum Angebot</label>
<input class="form-control" name="ItemLink" id="ItemLink" type="url" pattern="https?://.+">
<label class="form-label mt-2">Bild (URL)</label>
<input class="form-control" name="ItemImage" id="ItemImage" type="url" pattern="https?://.+">
<div class="form-check mt-2" id="RemoveImageWrap" style="display:none;">
<input class="form-check-input" type="checkbox" value="1" id="RemoveImage" name="RemoveImage">
<label class="form-check-label" for="RemoveImage">Aktuelles Bild entfernen</label>
</div>
</div>
<div class="modal-footer">
<input type="hidden" name="action" id="ItemAction" value="add">
<input type="hidden" name="WhishID" id="WhishID" value="-1">
<input type="hidden" name="ItemListUUID" value="<?= e($ListUUID) ?>">
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary" id="ItemSubmitBtn">Speichern</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Wunsch löschen</h5><button type="button" class="btn-close"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h5 id="del-whish-title">WunschTitel</h5>
<p>Soll dieser Wunsch wirklich gelöscht werden?</p>
</div>
<div class="modal-footer">
<form action="" method="POST">
<input type="hidden" name="WhishID" id="DelWhishID" value="-1">
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Nein</button>
<button type="submit" name="delete" class="btn btn-danger">Löschen</button>
</form>
</div>
</div>
</div>
</div>
<!-- Push Priority Modal -->
<div class="modal fade" id="pushprioModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Wunschpriorität</h5><button type="button" class="btn-close"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h5 id="prio-whish-title">WunschTitel</h5>
<p>Priorität ganz nach oben setzen?</p>
</div>
<div class="modal-footer">
<form action="" method="POST">
<input type="hidden" name="WhishID" id="PrioWhishID" value="-1">
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Nein</button>
<button type="submit" name="pushprio" class="btn btn-primary">Ja</button>
</form>
</div>
</div>
</div>
</div>
<!-- Reservation Modal (öffentlich) -->
<div class="modal fade" id="reservationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="reservationModalLabel">Wunsch reservieren</h5><button type="button"
class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="ReservationInfoText">Bitte vergeben Sie ein Passwort, um diesen Wunsch zu reservieren. Nur mit diesem
Passwort (oder durch den Listeneigentümer) kann die Reservierung wieder aufgehoben werden.</p>
<form action="reservations.php" method="POST" id="reservationForm">
<label for="WishPassword" class="form-label">Passwort</label>
<input type="password" class="form-control" id="WishPassword" name="WishPassword" required>
<input type="hidden" name="wishid" id="modal-wishid" value="">
<input type="hidden" name="reservedstat" id="modal-reservedstat" value="">
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary" form="reservationForm"
id="reservation-submit">Reservieren</button>
</div>
</div>
</div>
</div>
<!-- Create List Modal -->
<div class="modal fade" id="createListModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neue Liste anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<div class="mb-3">
<label for="listName" class="form-label">Titel der Liste</label>
<input type="text" class="form-control" id="listName" name="listName" required maxlength="255">
</div>
<div class="mb-3">
<label for="listDescription" class="form-label">Beschreibung (optional)</label>
<textarea class="form-control" id="listDescription" name="listDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="listPassword" class="form-label">Admin-Passwort (zum Bearbeiten)</label>
<input type="password" class="form-control" id="listPassword" name="listPassword" required>
<div class="form-text">
Dieses Passwort brauchst du später für Login/Bearbeitung der Liste.
</div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary" name="listadd">Anlegen</button>
</div>
</form>
</div>
</div>
</div>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/wishlist.js"></script>
</body>
</html>