Compare commits

...

3 Commits

Author SHA1 Message Date
b9d360542d many improvements 2025-08-18 20:39:36 +02:00
e0481df164 renamed file 2025-08-18 20:39:26 +02:00
4b8b3890d5 chore: ignore config.php; add config.sample.php template 2025-08-18 14:12:35 +02:00
22 changed files with 1870 additions and 677 deletions

7
.gitignore vendored
View File

@@ -1 +1,6 @@
data
# Wishlist secrets & local artifacts
config/config.php
data/
*.log
*.tmp
.cache/

View File

@@ -1,72 +0,0 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
include_once('config/config.php');
$ItemTitle = $_POST['ItemTitle'];
$ItemDescription = $_POST['ItemDescription'];
$ItemPrice = $_POST['ItemPrice'];
$ItemLink = $_POST['ItemLink'];
$ItemImage = $_POST['ItemImage'];
$ListID = $_POST['ItemListID'];
$nextPriority = 0;
#--- check if the provided Link is a valid URL
if (filter_var($ItemLink, FILTER_VALIDATE_URL) === FALSE) {
die('Not a valid URL');
}
#---
#--- check if the provided Image-Link is a real image:
$headers = array_change_key_case(get_headers($ItemImage, 1), CASE_LOWER); // make all keys LowerCase
if (strpos($headers['content-type'], 'image/') !== false) {
$strippedimagepath = strtok($ItemImage, '?');
$imageLocalLink = uniqid() . '.' . pathinfo($strippedimagepath, PATHINFO_EXTENSION);
echo "ImageLink: " . $imageLocalLink;
file_put_contents($imagedir . '/' . $imageLocalLink, fopen($strippedimagepath, 'r'));
} else {
echo "Link is Not an Image";
}
#---
$ItemPriceCents = floatval(str_replace(',', '.', str_replace('.', '', $ItemPrice))) * 100;
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
}
$stmt = 'SELECT MAX( priority ) AS maxprio FROM whishes WHERE whislist = ' . $ListID . ';';
$result = $conn->query($stmt);
while ($row = mysqli_fetch_array($result)) {
$nextPriority = $row['maxprio'] + 1;
}
$stmt = $conn->prepare('INSERT INTO whishes (title, description, link, image, price, whislist, priority) VALUES (?, ?, ?, ?, ?, ?, ?)');
if (false === $stmt) {
die('prepare() failed: ' . htmlspecialchars($conn->error));
}
$rc = $stmt->bind_param('ssssiii', $ItemTitle, $ItemDescription, $ItemLink, $imageLocalLink, $ItemPriceCents, $ListID, $nextPriority);
if (false === $rc) {
die('bind_param() failed: ' . htmlspecialchars($stmt->error));
}
$rc = $stmt->execute();
if (false === $rc) {
die('execute() failed: ' . htmlspecialchars($stmt->error));
}
$stmt->close();
$conn->close();
header('Location: ' . $_SERVER['HTTP_REFERER']);

View File

@@ -1,9 +0,0 @@
<?php
$servername = 'localhost';
$username = 'wishlist';
$db = 'wishlist';
$password = 'R!6CIb-KxM96EC]6';
$imagedir = 'data/images';
?>

45
config/config.sample.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/**
* Sample-Konfiguration für Simple Wishlist
* Kopiere diese Datei nach config.php und trage echte Werte ein.
*
* Sicherheitshinweise:
* - Lege die echte config.php NICHT ins Git-Repo (siehe .gitignore).
* - Setze Dateirechte restriktiv (z. B. 640) und Eigentümer auf den Webserver-User.
* - $image_host_whitelist: Wenn gesetzt, dürfen Bilder NUR von diesen Hosts gepullt werden.
*/
$servername = 'localhost';
$username = 'wishlist';
$password = 'yourcooldbpasshere';
$db = 'wishlist';
/**
* Pfad zum Bilder-Verzeichnis relativ zum Webroot oder absolut.
* Standard (relativ): 'data/images'
* Beispiel (absolut): '/var/www/wishlist/data/images'
*/
$imagedir = 'data/images';
/**
* Pfad zum Bilder-Verzeichnis für Platzhalter-Bilder zum Webroot oder absolut.
* Standard (relativ): 'img/placeholders'
* Beispiel (absolut): '/var/www/wishlist/img/placeholders'
*/
$placeholders_imagedir = 'img/placeholders';
/**
* (Optional) Whitelist für Bild-Downloads (SSRF-Schutz).
* - Leere Liste => kein Host explizit erlaubt, aber add_item.php blockt private/Loopback/Link-Local IPs.
* - Wenn du hier Einträge setzt, dürfen NUR diese Hosts (und deren Subdomains) genutzt werden.
* Beispiel: ['i.imgur.com', 'images.example.com']
*/
$image_host_whitelist = [];
/**
* (Optional) Produktions-Flags falls du sie in Templates brauchst.
* Die App selbst setzt error_reporting in index/add_item; hier nur als Doku.
*/
// $app_env = 'production'; // 'development' | 'production'
?>

9
css/fontawesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,80 +1,231 @@
/* ============ Base / Vars ============ */
@font-face {
font-family: 'Comfortaa';
font-family: "Comfortaa";
font-style: normal;
font-weight: 300;
src: url(../fonts/comfortaa.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url(../webfonts/comfortaa.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
:root {
--wl-card-radius: 0.75rem;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue",
Arial, "Comfortaa", sans-serif;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
/* Utility */
.object-fit-cover {
object-fit: cover;
}
.object-fit-contain {
object-fit: contain;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
/* ============ Cards ============ */
.card {
border-radius: var(--wl-card-radius);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
.card .ratio {
--bs-aspect-ratio: 56.25%; /* 16:9 Bildfläche */
border-top-left-radius: var(--wl-card-radius);
border-top-right-radius: var(--wl-card-radius);
background: #fff;
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.card-img-top {
width: 100%;
height: 15vw;
object-fit: scale-down;
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
height: 100%;
padding: 0;
display: block;
}
.card-body {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-height: 9rem;
}
/* Titelzeile */
.card-title-row {
gap: 0.5rem;
}
.card-title-row .card-title,
.card-title {
margin: 0 0 0.25rem 0;
font-weight: 700;
line-height: 1.15;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.qty-pill {
display: inline-block;
padding: 0.15rem 0.5rem;
font-size: 0.8rem;
line-height: 1;
border-radius: 999px;
background: var(--bs-gray-200);
color: var(--bs-gray-800);
}
/* Beschreibung (2 Zeilen Ellipsis) */
.card-text {
min-height: 3em;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 2.6em;
}
.card-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
/* Status rechts */
.status-right .fs-6 {
line-height: 1.2;
}
.card .text-end small,
.card .text-end .badge {
white-space: nowrap;
}
/* Header Controls */
.navbar-brand svg {
opacity: 0.9;
}
.navbar .form-control#sortby {
min-width: 10.5rem;
}
/* Demo-/Bootstrap-Beispielkram killen */
.bd-placeholder-img,
.bd-placeholder-img-lg,
.b-example-divider,
.b-example-vr,
.nav-scroller,
.nav-scroller .nav {
display: none !important;
}
/* ============ Admin-Icons (schwebend) ============ */
/* schwebende Admin-Icons sichtbar machen */
.wl-card {
position: relative;
} /* hast du schon */
.wl-card .admin-actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(2px);
border-radius: 999px;
padding: 0.125rem;
z-index: 10;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
/* beim Hovern (oder Tastaturfokus) einblenden */
.wl-card:hover .admin-actions {
opacity: 1;
transform: none;
}
/* --- Admin-Icon oben rechts bleibt kompakt --- */
.wl-card .btn-icon {
box-sizing: border-box;
width: 34px;
height: 34px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
}
/* ============ Untere Aktionsleiste ============ */
.btn-group-pills {
display: flex;
justify-content: center;
gap: 0.45rem;
flex-wrap: wrap;
align-items: center;
}
/* Idle: klein und fix 40px */
.btn-group-pills > .btn.btn-pill {
flex: 0 0 40px !important;
min-width: 40px; /* falls Bootstrap-Mindestbreiten reinfunken */
}
/* Hover/Fokus/Aktiv: darf wachsen */
.btn-group-pills > .btn.btn-pill:hover,
.btn-group-pills > .btn.btn-pill:focus,
.btn-group-pills > .btn.btn-pill:active {
flex: 0 1 16rem !important; /* überschreibt die Idle-Regel */
width: auto !important; /* falls irgendwo width:40px gewinnt */
padding: 0.5rem 0.8rem; /* „aufklappen“ */
justify-content: flex-start;
}
/* Text mit aufklappen */
.btn-pill:hover span,
.btn-pill:focus span,
.btn-pill:active span {
width: auto;
opacity: 1;
margin-left: 0.35rem;
}
/* kompakter Grundzustand: NUR Icon, super schmal */
.btn-pill {
box-sizing: border-box; /* Breite inkl. Padding & Border */
display: inline-flex;
align-items: center;
width: 40px;
height: 40px;
flex-basis: 40px;
padding: 0;
border-radius: 999px;
overflow: hidden;
white-space: nowrap;
}
.btn-pill i { font-size: 1rem; line-height: 1; margin: 0 .5rem; }
.btn-pill span {
width: 0; opacity: 0; overflow: hidden; white-space: nowrap; margin-left: 0;
transition: width .18s ease, opacity .18s ease, margin-left .18s ease;
}
/* Hover: Button wächst, Text klappt auf */
.btn-pill:hover {
width: auto;
flex: 0 1 16rem;
padding: .5rem .8rem;
/* Keine Ausrichtung hier setzen die Utility-Klasse bleibt wirksam */
}
.btn-pill:hover span { width: auto; opacity: 1; margin-left: .35rem; }
/* Fokus sichtbar (zugänglich) */
.btn-pill:focus-visible {
outline: 2px solid rgba(13, 110, 253, 0.5);
outline-offset: 2px;
}
/* ============ Responsive Tweaks ============ */
@media (max-width: 575.98px) {
.card-body {
min-height: unset;
}
.btn-group-pills {
gap: 0.35rem;
}
.btn-pill {
width: 38px;
flex-basis: 38px;
}
}

118
deploy.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env bash
set -euo pipefail
# =========================
# Wishlist Deploy (SFTP)
# =========================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="${SOURCE_DIR:-"$SCRIPT_DIR/"}"
TARGET_DIR="${TARGET_DIR:-"/public_html/wishlist.hiabuto.de"}"
SFTP_HOST="${SFTP_HOST:-www374.your-server.de}"
SFTP_USER="${SFTP_USER:-peterksd}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
DRY_RUN="${DRY_RUN:-0}"
# Glob-Excludes (rein mit Globs, kompatibel zu älteren lftp-Versionen)
EXCLUDE_ARGS=(
# Git-Kram (rekursiv, egal wo)
--exclude-glob ".git"
--exclude-glob ".git/*"
--exclude-glob "*/.git"
--exclude-glob "*/.git/*"
--exclude-glob "**/.git"
--exclude-glob "**/.git/*"
--exclude-glob ".git*"
--exclude-glob "*/.git*"
--exclude-glob "**/.git*"
--exclude-glob ".gitattributes"
--exclude-glob ".gitignore"
--exclude-glob ".github*"
# Secrets/Meta
--exclude-glob ".env*"
--exclude-glob "deploy*.sh"
--exclude-glob "README*"
--exclude-glob "*.md"
--exclude-glob "*.sql"
# Vendor/Node
--exclude-glob "node_modules"
--exclude-glob "node_modules/**"
--exclude-glob "vendor/*/.git*"
# Deine echte Config NICHT überschreiben
--exclude-glob "config/config.php"
# Server-Daten (Bilder) NICHT anfassen
--exclude-glob "data"
--exclude-glob "data/*"
--exclude-glob "data/**"
)
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
echo -e "${GREEN}Starting wishlist deployment...${NC}"
echo -e "${GREEN}SFTP Upload -> ${SFTP_USER}@${SFTP_HOST}${NC}"
echo -e "${GREEN}Target Dir -> ${TARGET_DIR}${NC}"
(( DRY_RUN == 1 )) && echo -e "${YELLOW}Mode -> DRY-RUN${NC}"
need() { command -v "$1" >/dev/null 2>&1 || { echo -e "${RED}Error: '$1' ist nicht installiert.${NC}"; exit 1; }; }
build_connect_program() {
printf "ssh -i %q -o IdentitiesOnly=yes -o PreferredAuthentications=publickey -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no -o NumberOfPasswordPrompts=0 -o BatchMode=yes -o ConnectTimeout=15 -o LogLevel=ERROR" "$SSH_KEY"
}
need lftp
[ -d "$SOURCE_DIR" ] || { echo -e "${RED}Error: SOURCE_DIR existiert nicht: ${SOURCE_DIR}${NC}"; exit 1; }
[ -r "$SSH_KEY" ] || { echo -e "${RED}Error: SSH-Key nicht gefunden/lesbar: ${SSH_KEY}${NC}"; exit 1; }
case "$TARGET_DIR" in
/public_html/*) : ;;
*) echo -e "${RED}TARGET_DIR muss unter /public_html/ liegen (aktuell: ${TARGET_DIR})${NC}"; exit 1;;
esac
echo -e "${YELLOW}>> Prüfe SFTP-Verbindung (key-only)...${NC}"
if ! lftp </dev/null -e "
set cmd:interactive false;
set cmd:fail-exit yes;
set net:max-retries 1;
set net:timeout 15;
set sftp:auto-confirm yes;
set sftp:connect-program '$(build_connect_program)';
open -u ${SFTP_USER}, sftp://${SFTP_HOST};
cls -1 '${TARGET_DIR}';
bye
" >/dev/null 2>&1; then
echo -e "${RED}SFTP-Test fehlgeschlagen.${NC}"
exit 255
fi
echo -e "${YELLOW}>> Upload per SFTP (mirror -R)...${NC}"
MIRROR_OPTS=( -R --delete --verbose --parallel=4 )
(( DRY_RUN == 1 )) && MIRROR_OPTS+=( --dry-run )
(( ${DEBUG:-0} == 1 )) && {
echo "mirror opts: ${MIRROR_OPTS[*]}"
echo "exclude : ${EXCLUDE_ARGS[*]}"
}
lftp -e "
set cmd:interactive false;
set cmd:fail-exit yes;
set net:max-retries 2;
set net:timeout 20;
set sftp:auto-confirm yes;
set sftp:connect-program '$(build_connect_program)';
open -u ${SFTP_USER}, sftp://${SFTP_HOST};
mirror ${MIRROR_OPTS[*]} ${EXCLUDE_ARGS[*]} '${SOURCE_DIR%/}/' '${TARGET_DIR%/}/';
bye
"
if (( DRY_RUN == 1 )); then
echo -e "${GREEN}DRY-RUN erfolgreich (keine Dateien verändert).${NC}"
else
echo -e "${GREEN}SFTP-Upload erfolgreich.${NC}"
fi
echo -e "${GREEN}Deployment completed.${NC}"

BIN
img/no-image-katie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -1,32 +1,116 @@
<?php
declare(strict_types=1);
include_once('../config/config.php');
$dir = new DirectoryIterator('../' . $imagedir);
/**
* delete_unused.php
*
* Löscht Bilddateien aus $imagedir, die in der DB nicht referenziert sind.
* Sicherheit:
* - CLI-only (kein Webzugriff)
* - Pfad-Härtung via realpath()
* - Dry-Run optional (default)
*
* Aufruf:
* php delete_unused.php # Dry-Run (zeigt nur an)
* php delete_unused.php --apply # wirklich löschen
*/
$conn = new mysqli($servername, $username, $password, $db);
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit('Forbidden');
}
require_once __DIR__ . '/../config/config.php';
function out(string $msg): void
{
fwrite(STDOUT, $msg . PHP_EOL);
}
function err(string $msg): void
{
fwrite(STDERR, "ERROR: " . $msg . PHP_EOL);
exit(1);
}
// Flags
$apply = in_array('--apply', $argv, true);
// DB verbinden
$conn = @new mysqli($GLOBALS['servername'], $GLOBALS['username'], $GLOBALS['password'], $GLOBALS['db']);
if ($conn->connect_error)
die('Connection failed: ' . $conn->connect_error);
err('DB-Verbindung fehlgeschlagen');
$sql = 'SELECT image FROM whishes';
$result = $conn->query($sql);
// Bildverzeichnis prüfen/härten
$baseDir = realpath(__DIR__ . '/..');
if ($baseDir === false)
err('BaseDir nicht gefunden');
if ($result !== false && $result->num_rows > 0)
{
if ($rows = $result->fetch_all())
{
foreach ($dir as $fileinfo) {
if (!$fileinfo->isDot()) {
$filename = $fileinfo->getFilename();
$imgDirCfg = rtrim((string) $GLOBALS['imagedir'], '/');
$imgDir = realpath(__DIR__ . '/../' . $imgDirCfg);
if ($imgDir === false)
err('imagedir nicht gefunden: ' . $imgDirCfg);
if (!in_array($filename, $rows))
{
$deletepath = '../' . $imagedir . '/' . $filename;
unset($deletepath);
}
}
}
}
// Verhindere, dass außerhalb des Projekts gelöscht wird
if (strpos($imgDir, $baseDir) !== 0)
err('imagedir liegt außerhalb des Projekts: ' . $imgDir);
// Aus DB: genutzte Dateien (NULL/"" filtern)
$used = [];
$sql = 'SELECT image FROM wishes WHERE image IS NOT NULL AND image <> ""';
$res = $conn->query($sql);
if ($res === false)
err('Query fehlgeschlagen');
while ($row = $res->fetch_assoc()) {
$used[] = (string) $row['image'];
}
$res->free();
$conn->close();
// In ein Set packen
$usedSet = array_flip($used);
$deleted = 0;
$kept = 0;
$skipped = 0;
$it = new DirectoryIterator($imgDir);
foreach ($it as $fileinfo) {
if ($fileinfo->isDot())
continue;
$path = $fileinfo->getPathname();
if ($fileinfo->isDir()) {
$skipped++;
out("[skip-dir] " . $path);
continue;
}
$conn->close();
$name = $fileinfo->getFilename();
if (isset($usedSet[$name])) {
$kept++;
out("[keep] " . $name);
continue;
}
// Nicht referenziert -> löschen (oder dry-run)
if ($apply) {
if (@unlink($path)) {
$deleted++;
out("[delete] " . $name);
} else {
err('Konnte Datei nicht löschen: ' . $path);
}
} else {
out("[dry-run] " . $name . " (würde gelöscht)");
}
}
out("");
out("Summary:");
out(" kept: {$kept}");
out(" skipped: {$skipped}");
out(" deleted: {$deleted}" . ($apply ? "" : " (dry-run)"));
exit(0);

View File

@@ -1,4 +0,0 @@
<?php
if(isset($_GET['pass'])) {
echo(password_hash($_GET['pass'], PASSWORD_DEFAULT));
}

View File

@@ -1,162 +1,332 @@
<?php
declare(strict_types=1);
include 'config/config.php';
// Konfiguration einbinden (stellt $servername, $username, $password, $db, $imagedir bereit)
require_once __DIR__ . '/../config/config.php';
function generateListItem($ListItemID, $ItemImage, $ItemTitle, $ItemLink, $ItemPrice, $ItemComment, $ItemReserved, $ItemDate)
/**
* HTML-Escape Helper (fallback, falls global e() fehlt)
*/
if (!function_exists('e')) {
function e(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
/** Attribut-Helper (für data-*, value, title, …) */
if (!function_exists('attr')) {
function attr(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
/** Pfad zum Platzhalterbild ermitteln */
/** Pfad zum Platzhalterbild ermitteln */
if (!function_exists('lg_default_image')) {
function lg_default_image(): string
{
$dir = $_SERVER['DOCUMENT_ROOT'] . '/' . ($GLOBALS['placeholders_imagedir'] ?? 'img/placeholders');
// Alle PNG/JPG Dateien aus dem Ordner holen
$files = glob($dir . '/*.{png,jpg,jpeg,gif,webp}', GLOB_BRACE);
if ($files && count($files) > 0) {
// Einen zufälligen auswählen
$randomFile = $files[array_rand($files)];
// In relative URL umwandeln
$relativePath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $randomFile);
return $relativePath;
}
// Fallback: statisches no-image
return '/img/no-image-katie-1.png';
}
}
/**
* DB-Connector (keine Fehlerdetails nach außen)
*/
function lg_db(): mysqli
{
global $servername, $username, $password, $db;
$conn = new mysqli($servername, $username, $password, $db);
if ($conn->connect_error) {
http_response_code(500);
exit('Interner Fehler (DB)');
}
$conn->set_charset('utf8mb4');
return $conn;
}
/**
* Einzelne Karten-Kachel rendern
*/
function generateListItem(
int $ListItemID,
?string $ItemImage,
string $ItemTitle,
?string $ItemLink,
int $ItemPriceCents,
?string $ItemComment,
int $ItemReserved,
?string $ItemDate,
int $ItemQty,
): void {
global $loggedin, $imagedir;
$formatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
if (strlen($ItemComment) == 0) {
$ItemComment = '&nbsp;';
// Preis formatieren (de_DE)
$priceText = '';
if ($ItemPriceCents > 0) {
if (class_exists('NumberFormatter')) {
$fmt = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
$priceText = $fmt->formatCurrency($ItemPriceCents / 100, 'EUR');
} else {
$priceText = number_format($ItemPriceCents / 100, 2, ',', '.') . ' €';
}
}
echo ('
// Kommentar (2 Zeilen max., CSS macht den Clamp)
$commentPlain = trim((string) $ItemComment);
$commentHtml = ($commentPlain === '') ? '&nbsp;' : e($commentPlain);
// Bild (lokal oder Placeholder)
$srcPath = '';
if (!empty($ItemImage)) {
$local = rtrim($imagedir, '/') . '/' . $ItemImage;
$srcPath = @is_file($local) ? $local : lg_default_image();
} else {
$srcPath = lg_default_image();
}
$imageTag = '<div class="ratio ratio-16x9"><img src="' . e($srcPath) . '" class="card-img-top object-fit-contain" alt="Bild zum Wunsch"></div>';
// Titelzeile: Qty-Pill (nur wenn >1) + einzeiliger Titel
$qtyPill = ($ItemQty > 1) ? '<span class="qty-pill me-2">×' . (int) $ItemQty . '</span>' : '';
$titleHtml = '<div class="d-flex align-items-center card-title-row">'
. $qtyPill
. '<h5 class="card-title flex-grow-1 mb-0">' . e($ItemTitle) . '</h5>'
. '</div>';
// Status-Badge links
$statusLeft = '';
if ($ItemReserved >= $ItemQty) {
$statusLeft = '<span class="badge bg-danger">alle reserviert</span>';
} elseif ($ItemReserved > 0) {
$statusLeft = '<span class="badge bg-warning text-dark">'
. (int) $ItemReserved . ' / ' . (int) $ItemQty . ' reserviert</span>';
}
// Datum (lokal lesbar)
$dateHtml = '';
if (!empty($ItemDate)) {
$ts = strtotime($ItemDate);
$dateHtml = $ts ? date('d.m.Y', $ts) : e($ItemDate);
}
// User-Buttons (Reserve / Cancel / Vendor)
// --- unten: Aktionsleiste (Icons, wachsen bei Hover) ---
// (Reserve nur wenn möglich; Cancel immer; Vendor nur wenn Link)
$userBtns = [];
if ($ItemReserved < $ItemQty) {
$userBtns[] = sprintf(
'<button class="btn btn-primary btn-pill justify-content-end"
aria-label="Reservieren"
data-bs-toggle="modal" data-bs-target="#reservationModal"
data-wishid="%d" data-reserved="0">
<i class="fa-solid fa-lock"></i>
<span>Reservieren</span>
</button>',
$ListItemID
);
}
if ($ItemReserved > 0) {
$userBtns[] = sprintf(
'<button class="btn btn-outline-primary btn-pill justify-content-center"
aria-label="Reservierung aufheben"
data-bs-toggle="modal" data-bs-target="#reservationModal"
data-wishid="%d" data-reserved="1">
<i class="fa-solid fa-unlock"></i>
<span>Reservierung aufheben</span>
</button>',
$ListItemID
);
}
if (!empty($ItemLink)) {
$safeLink = e($ItemLink);
$userBtns[] =
'<a class="btn btn-outline-dark btn-pill justify-content-start"
aria-label="Zum Anbieter"
href="' . $safeLink . '" target="_blank" rel="noopener">
<i class="fa-solid fa-up-right-from-square">
</i><span>Zum Anbieter</span>
</a>';
}
$userBtnRow = implode("\n", $userBtns);
// ... in deinem Echo-Heredoc:
// <div class="btn-group btn-group-pills mt-3" role="group" aria-label="Aktionen">
// {$userBtnRow}
// </div>
// Admin-Icons (nur eingeloggt)
$adminFloating = '';
if (!empty($loggedin)) {
$adminFloating = '
<div class="admin-actions btn-group btn-group-sm" role="group" aria-label="Admin-Action">
<button class="btn btn-outline-secondary btn-icon" title="nach oben"
data-bs-toggle="modal" data-bs-target="#pushprioModal" data-wishid="' . (int) $ListItemID . '">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button class="btn btn-outline-warning btn-icon" title="edit"
data-bs-toggle="modal" data-bs-target="#itemModal"
data-mode="edit"
data-wishid="' . (int) $ListItemID . '"
data-title="' . attr($ItemTitle) . '"
data-description="' . attr($commentPlain) . '"
data-price="' . attr(($ItemPriceCents > 0) ? number_format($ItemPriceCents / 100, 2, ',', '.') : '') . '"
data-qty="' . (int) $ItemQty . '"
data-link="' . attr((string) $ItemLink) . '">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn btn-outline-danger btn-icon" title="löschen"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-wishid="' . (int) $ListItemID . '">
<i class="fa-solid fa-trash"></i>
</button>
</div>';
}
// Preis-Badge rechts
$priceHtml = $priceText !== '' ? '<span class="badge text-bg-light fs-6 fw-semibold">' . e($priceText) . '</span>' : '';
echo <<<HTML
<div class="col">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="card-title">' . $ItemTitle . '</h5>
</div>
<img src="' . $imagedir . '/' . $ItemImage . '" class="card-img-top">
<div class="card-body">
<p class="card-text">' . $ItemComment . '</p>
<div class="row justify-content-end">
<small class="text-muted text-end">' . $formatter->formatCurrency($ItemPrice / 100, 'EUR') . '</small>
</div>
</div>
<div class="card-footer text-muted">
<div class="d-flex justify-content-between">
<div class="d-inline btn-group">
<a href="' . $ItemLink . '" class="btn btn-sm btn-outline-secondary" role="button" target="_blank">zum Anbieter</a>
<button type="button" class="btn btn-sm ' . ($ItemReserved == true ? 'btn-outline-info' : 'btn-outline-secondary') . '" data-reserved="' . $ItemReserved . '" data-wishid="' . $ListItemID . '" data-bs-toggle="modal" data-bs-target="#reservationModal">' . ($ItemReserved == true ? 'Reservierung aufheben' : 'Reservieren') . '</button>');
<div class="card wl-card shadow-sm h-100 position-relative">
{$adminFloating}
{$imageTag}
<div class="card-body d-flex flex-column">
{$titleHtml}
<p class="card-text">{$commentHtml}</p>
if($loggedin == true)
{
echo('
<button type="button" class="btn btn-sm btn-outline-danger" data-wishid="' . $ListItemID . '" data-bs-toggle="modal" data-bs-target="#deleteModal">Löschen</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-wishid="' . $ListItemID . '" data-bs-toggle="modal" data-bs-target="#pushprioModal">Prio +</button>'
);
}
echo('
</div>
<div class="d-inline">
<small class="text-muted text-end">' . date('d.m.y', strtotime($ItemDate)) . '</small>
<div class="d-flex justify-content-between align-items-center mt-1">
<div>{$statusLeft}</div>
<div class="status-right text-end">
<div>{$priceHtml}</div>
<div class="text-muted small">{$dateHtml}</div>
</div>
</div>
<div class="btn-group btn-group-pills mt-3" role="group" aria-label="Aktionen">
{$userBtnRow}
</div>
</div>
</div>
</div>
');
HTML;
}
function wishlistMainBuilder($ListID, $sortby)
/**
* Haupt-Builder für die Listenansicht
* $ListID: aktive Liste (int, intern)
* $sortby: einer aus der Whitelist unten
*/
function wishlistMainBuilder(int $ListID, string $sortby = 'priority'): void
{
global $loggedin;
global $servername, $username, $password, $db;
$conn = lg_db();
// Create connection
$conn = new mysqli($servername, $username, $password, $db);
// 1) Listen-Metadaten holen
$stmt = $conn->prepare('SELECT title, description FROM lists WHERE ID = ?');
$stmt->bind_param('i', $ListID);
$stmt->execute();
$res = $stmt->get_result();
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
$listTitle = 'Unbekannte Liste';
$listDesc = '';
if ($res && $row = $res->fetch_assoc()) {
$listTitle = e((string) $row['title']);
$listDesc = e((string) $row['description']);
}
$stmt->close();
$sql = 'SELECT title, description FROM lists WHERE ID = ' . $ListID;
$result = $conn->query($sql);
// 2) ORDER BY Whitelist
$orderWhitelist = [
'priority' => 'priority DESC',
'price_asc' => 'price ASC',
'price_desc' => 'price DESC',
'date_desc' => 'date DESC',
'date_asc' => 'date ASC',
'random' => 'RAND()',
];
$orderSql = $orderWhitelist[$sortby] ?? $orderWhitelist['priority'];
echo ('
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
');
// 3) Wünsche laden
$sql = "
SELECT w.ID, w.image, w.title, w.link, w.price, w.description,
w.date, w.qty,
(SELECT COUNT(*) FROM wishes_reservations r WHERE r.wish_id = w.ID) AS reserved_count
FROM wishes w
WHERE w.wishlist = ?
ORDER BY {$orderSql}";
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $ListID);
$stmt->execute();
$items = $stmt->get_result();
if ($result !== false && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
echo ('
<h1 class="fw-light">' . $row['title'] . '</h1>
<p class="lead text-muted">' . $row['description'] . '</p>
');
}
} else {
echo ('
<div class="modal-dialog" role="document">
<div class="modal-content rounded-4 shadow">
<div class="modal-header p-5 pb-4 border-bottom-0">
<h1 class="fw-bold mb-0 fs-2">Das tut mir leid...</h1>
<p class="modal-title fs-5" >..aber diese Liste exisiert nicht. Möchten Sie eine neue anlegen ?</p>
</div>
// 4) Header-Text (vorher berechnen, kein Ternary im Heredoc)
$loginMsg = $loggedin
? 'Eingeloggt: Du kannst Einträge bearbeiten, löschen und priorisieren.'
: 'Tipp: Du kannst Einträge reservieren. Nur mit deinem Reservierungs-Passwort lässt sich die Reservierung wieder lösen.';
<div class="modal-body p-5 pt-0">
<form action="" method="POST">
<div class="form-floating mb-3">
<input type="text" class="form-control rounded-3" id="listName" name="listName" placeholder="Name der Liste">
<label for="listName">Name der Liste</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control rounded-3" id="listPassword" name="listPassword" placeholder="Password">
<label for="listPassword">Password</label>
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control rounded-3" id="listDescription" name="listDescription" placeholder="Beschreibung">
<label for="listDescription">Beschreibung</label>
</div>
<button class="w-100 mb-2 btn btn-lg rounded-3 btn-primary" name="listadd" type="submit">Absenden</button>
</form>
</div>
echo <<<HTML
<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">{$listTitle}</h1>
<p class="lead text-muted">{$listDesc}</p>
<p class="text-muted">{$loginMsg}</p>
</div>
');
}
</div>
</section>
echo ('
</div></div></section>
');
// End of Header Generator
echo ('
<div class="album py-5 bg-light">
<div class="container">
<div class="album py-3 bg-light">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
');
HTML;
$sort = 'id';
switch ($sortby) {
case 'price_asc':
$sort = 'price ASC';
break;
case 'price_desc':
$sort = 'price DESC';
break;
case 'date_desc':
$sort = 'date DESC';
break;
case 'date_asc':
$sort = 'date ASC';
break;
case 'random':
$sort = 'RAND()';
break;
case 'priority':
$sort = 'priority DESC';
break;
}
$sql = 'SELECT ID, title, description, link, image, reserved, price, date FROM whishes WHERE whislist = ' . $ListID . ' ORDER BY ' . $sort;
$result = $conn->query($sql);
if ($result !== false && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
generateListItem($row['ID'], $row['image'], $row['title'], $row['link'], $row['price'], $row['description'], $row['reserved'], $row['date']);
if ($items !== false && $items->num_rows > 0) {
while ($row = $items->fetch_assoc()) {
generateListItem(
(int) $row['ID'],
$row['image'] !== null ? (string) $row['image'] : null,
(string) $row['title'],
$row['link'] !== null ? (string) $row['link'] : null,
(int) $row['price'],
$row['description'] !== null ? (string) $row['description'] : null,
(int) $row['reserved_count'], // echte Anzahl Reservierungen
$row['date'] !== null ? (string) $row['date'] : null,
isset($row['qty']) ? (int) $row['qty'] : 1
);
}
} else {
echo '<div class="col"><div class="alert alert-info w-100">Keine Einträge vorhanden.</div></div>';
}
echo ('
</div></div></div>
');
echo <<<HTML
</div>
</div>
</div>
HTML;
$stmt->close();
$conn->close();
}

738
index.php
View File

@@ -1,217 +1,265 @@
<?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();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
include_once('include/listgenerator.php');
include_once('config/config.php');
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;
$loggedin = false;
$sortby = 'priority';
$ListUUID = '';
if (isset($_GET['list'])) {
$ListID = $_GET['list'];
}
if (isset($_POST['sortby'])) {
$sortby = $_POST['sortby'];
} else if (isset($_POST['sortby_transfer'])) {
$sortby = $_POST['sortby_transfer'];
}
if (isset($_SESSION['listid'])) {
if ($ListID == $_SESSION['listid']) {
$loggedin = true;
$raw = trim((string) $_GET['list']);
if (preg_match('/^[0-9a-fA-F-]{32,36}$/', $raw)) {
$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'];
}
$s->close();
$c->close();
} else {
$ListID = (int) $raw;
if ($ListID >= 0) {
$c = db();
$s = $c->prepare('SELECT uuid FROM lists WHERE ID=?');
$s->bind_param('i', $ListID);
$s->execute();
$r = $s->get_result();
if ($r && ($row = $r->fetch_assoc())) {
$ListUUID = (string) $row['uuid'];
$scheme = $secure ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? '';
header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($ListUUID), true, 301);
exit;
}
$s->close();
$c->close();
}
}
}
if (isset($_POST['login'])) {
$ListPassword = $_POST['ListPassword'];
$ListID = $_POST['ListID'];
$conn = new mysqli($servername, $username, $password, $db);
// ===== 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';
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
}
// ===== Login-Status =====
$loggedin = (isset($_SESSION['listid']) && $ListID === (int) $_SESSION['listid']);
$GLOBALS['loggedin'] = $loggedin; // für listgenerator.php
$sql = 'SELECT edit_pw FROM lists WHERE ID = ' . $ListID;
$result = $conn->query($sql);
// ===== POST-Actions (mit CSRF) =====
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
if ($result !== false && $result->num_rows > 0) {
if ($row = $result->fetch_assoc()) {
if (password_verify($ListPassword, $row['edit_pw'])) {
$_SESSION['listid'] = $ListID;
// 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;
$message = array('msg' => 'Login erfolgreich', 'type' => 'success');
} else {
$message = array('msg' => 'Falsches Passwort', 'type' => 'warning');
}
$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'];
}
}
$conn->close();
}
if (isset($_POST['listadd'])) {
$listName = $_POST['listName'];
$listPassword = password_hash($_POST['listPassword'], PASSWORD_DEFAULT);
$listDescription = $_POST['listDescription'];
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
$s->close();
$c->close();
}
$sql = 'INSERT INTO lists (title, description, edit_pw) VALUES ("' . $listName . '", "' . $listDescription . '","' . $listPassword . '")';
$result = $conn->query($sql);
if ($conn->query($sql) === TRUE) {
$last_id = $conn->insert_id;
$_SESSION['listid'] = $last_id;
$loggedin = true;
$actual_link = 'http://' . $_SERVER['HTTP_HOST'] . '/?list=' . $last_id;
header('Location: ' . $actual_link);
} else {
$message = array('msg' => 'Error: ' . $sql . '<br>' . $conn->error, 'type' => 'error');
// LOGOUT
if (isset($_POST['logout'])) {
session_destroy();
$loggedin = false;
$message = ['msg' => 'Logout erfolgreich', 'type' => 'success'];
}
$conn->close();
}
if (isset($_POST['logout'])) {
session_destroy();
$loggedin = false;
$message = array('msg' => 'Logout erfolgreich', 'type' => 'success');
}
if (isset($_POST['reservation'])) {
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
// 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();
}
if ($_POST['reservedstat'] == 1) {
$sql = 'SELECT reserved_pw FROM whishes WHERE ID = ' . $_POST['wishid'];
$result = $conn->query($sql);
if ($result !== false && $result->num_rows > 0) {
if ($row = $result->fetch_assoc()) {
if (password_verify($_POST['WishPassword'], $row['reserved_pw'])) {
$sql = 'UPDATE whishes SET reserved=0, reserved_pw="" WHERE ID = ' . $_POST['wishid'];
if ($conn->query($sql) === TRUE)
$message = array('msg' => 'Reservierung aufgehoben', 'type' => 'success');
else
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
} else {
$message = array('msg' => 'Falsches Reservierungs-Passwort', 'type' => 'warning');
}
}
}
}
if ($_POST['reservedstat'] == 0) {
$reservedHash = password_hash($_POST['WishPassword'], PASSWORD_BCRYPT);
$sql = 'UPDATE whishes SET reserved=1, reserved_pw="' . $reservedHash . '" WHERE ID = ' . $_POST['wishid'];
if ($conn->query($sql) === TRUE)
$message = array('msg' => 'Reservierung eingetragen', 'type' => 'success');
else
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
}
$conn->close();
}
if (isset($_POST['pushprio'])) {
$nextPriority = 0;
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
}
$stmt = 'SELECT MAX( priority ) AS maxprio FROM whishes WHERE whislist = ' . $ListID . ';';
$result = $conn->query($stmt);
while ($row = mysqli_fetch_array($result)) {
$nextPriority = $row['maxprio'] + 1;
}
$sql = 'UPDATE whishes SET priority=' . $nextPriority . ' WHERE ID = ' . $_POST['WhishID'];
if ($conn->query($sql) === TRUE)
$message = array('msg' => 'Wunschpriorität aktualisiert', 'type' => 'success');
else
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
}
if (isset($_POST['delete']) && $loggedin == true) {
if (isset($_POST['WhishID'])) {
$WhishID = $_POST['WhishID'];
// LÖSCHEN (nur eingeloggt)
if (isset($_POST['delete']) && $loggedin === true) {
$WhishID = (int) ($_POST['WhishID'] ?? -1);
$WhishTitle = '';
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
}
$sql = 'SELECT image, title FROM whishes WHERE ID = ' . $WhishID;
$result = $conn->query($sql);
if ($result !== false && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
unlink($imagedir . '/' . $row['image']);
$WhishTitle = $row['title'];
$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);
}
}
$sql = 'DELETE FROM whishes WHERE ID = ' . $WhishID;
if ($conn->query($sql) === TRUE)
$message = array('msg' => 'Wunsch <b>"' . $WhishTitle . '"</b> gelöscht', 'type' => 'success');
else
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
} else {
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
$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="en">
<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/custom.css">-->
<link rel="stylesheet" href="css/tweaks.css">
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/jquery.min.js"></script>
<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="manifest" href="img/site.webmanifest">
<link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="img/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="img/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="manifest" href="img/site.webmanifest">
</head>
<body>
@@ -220,39 +268,36 @@ if (isset($_POST['delete']) && $loggedin == true) {
<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 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">
<?php
if ($loggedin == true) {
echo ('
<?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">
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal" data-bs-target="#addItemModal">Add Item</button>
<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>
<form class="form-inline" action="" method="POST">
<button type="submit" class="btn btn-outline-secondary my-2 my-sm-0" name="logout">Logout</button>
</form>
');
} else {
echo ('
<form class="form-inline" action="" method="POST">
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal" data-bs-target="#loginModal">Login</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 <?php echo ($sortby == 'priority' ? 'selected="selected"' : ''); ?> value="priority">Priorit&auml;t</option>
<option <?php echo ($sortby == 'price_asc' ? 'selected="selected"' : ''); ?> value="price_asc">Preis aufsteigend</option>
<option <?php echo ($sortby == 'price_desc' ? 'selected="selected"' : ''); ?> value="price_desc">Preis absteigend</option>
<option <?php echo ($sortby == 'date_desc' ? 'selected="selected"' : ''); ?> value="date_desc">Datum, neu -> alt</option>
<option <?php echo ($sortby == 'date_asc' ? 'selected="selected"' : ''); ?> value="date_asc">Datum, alt -> neu</option>
<option <?php echo ($sortby == 'random' ? 'selected="selected"' : ''); ?> value="random">Zufall</option>
<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>
</div>
@@ -261,158 +306,141 @@ if (isset($_POST['delete']) && $loggedin == true) {
</div>
</header>
<main>
<?php
if (isset($message)) {
echo ('
<div class="alert alert-' . $message['type'] . ' alert-dismissible fade show" role="alert">
' . $message['msg'] . '
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<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>
');
}
wishlistMainBuilder($ListID, $sortby);
?>
<?php endif; ?>
<?php wishlistMainBuilder($ListID, $sortby); ?>
</main>
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="float-end mb-1"><a href="#">Back to top</a></p>
<p class="mb-1">Simple Wishlist &copy; by Marcel Peterkau</p>
</div>
</footer>
<?php
if ($loggedin == true) {
echo ('
<!-- Modal addItem-->
<div class="modal fade" id="addItemModal" tabindex="-1" aria-labelledby="addItemModalLabel" aria-hidden="true">
<?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" id="addItemModalLabel">Add new Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="add_item.php" method="POST">
<div class="modal-body">
<label for="ItemTitle" class="form-label">Titel</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="ItemTitle" name="ItemTitle" rows="3" required>
</div>
<label for="ItemDescription" class="form-label">Beschreibung</label>
<div class="input-group mb-3">
<textarea class="form-control" id="ItemDescription" name="ItemDescription" rows="3"></textarea>
</div>
<label for="ItemPrice" class="form-label">Preis</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="ItemPrice" name="ItemPrice" pattern="^\d*(\,\d{2}$)?" value="" data-type="currency" placeholder="0,00€" />
<span class="input-group-text">€</span>
</div>
<label for="ItemLink" class="form-label">Link zum Angebot</label>
<div class="input-group mb-3">
<input type="url" class="form-control" id="ItemLink" name="ItemLink" pattern="https?://.+" title="Include http://" rows="3">
</div>
<label for="ItemImage" class="form-label">Link zum Bild</label>
<div class="input-group mb-3">
<input type="url" class="form-control" id="ItemImage" name="ItemImage" pattern="https?://.+" title="Include http://" rows="3">
</div>
</div>
<div class="modal-footer">
<input type="hidden" id="ItemListID" name="ItemListID" value="' . $ListID . '">
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Add new Item</button>
</div>
</form>
</div>
</div>
</div>
');
}
if ($loggedin != true) {
echo ('
<!-- Modal Login-->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">Login</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<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>
<div class="input-group mb-3">
<input type="password" class="form-control" id="ListPassword" name="ListPassword" rows="3" required>
<input type="hidden" id="ListID" name="ListID" value="' . $ListID . '">
<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>
<div class="modal-footer">
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" name="login" class="btn btn-primary">Login</button>
</div>
</form>
<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>
</div>
');
}
<?php endif; ?>
if ($loggedin == true) {
echo ('
<!-- Modal Delete-->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<!-- 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="deleteModalLabel">Wunsch löschen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<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="whish-title">WunschTitel</h5>
<p>Soll dieser Wunsch wirklich gelöscht werden ?</p>
<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" id="WhishID" name="WhishID" value="-1">
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" name="delete" class="btn btn-primary">Löschen</button>
<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>
<!-- END OF Modal Delete-->
<!-- Modal PushPrio-->
<div class="modal fade" id="pushprioModal" tabindex="-1" aria-labelledby="pushprioModalLabel" aria-hidden="true">
<!-- 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" id="pushprioModalLabel">Wunschpriorität</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<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="whish-title">WunschTitel</h5>
<p>Soll die Priorität dieses Wunsch ganz nach oben gesetzt werden?</p>
<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" id="WhishID" name="WhishID" value="-1">
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
<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>
@@ -420,91 +448,39 @@ if (isset($_POST['delete']) && $loggedin == true) {
</div>
</div>
</div>
<!-- END OF Modal PushPrio-->
');
}
?>
<!-- Modal Reservation-->
<div class="modal fade" id="reservationModal" tabindex="-1" aria-labelledby="reservationModalLabel" aria-hidden="true">
<!-- 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" aria-label="Close"></button>
<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="" method="POST">
<label for="ListPassword" class="form-label">Passwort</label>
<div class="input-group mb-3">
<input type="password" class="form-control" id="WishPassword" name="WishPassword" rows="3" required>
<input type="hidden" name="wishid" id="modal-wishid" value="">
<input type="hidden" name="reservedstat" id="modal-reservedstat" value="">
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
</div>
<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">Close</button>
<button type="submit" id="reservation-submit" name="reservation" class="btn btn-primary">Reservieren</button>
</form>
<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>
<!-- END OF Modal Reservation-->
<script>
$('#reservationModal').on('show.bs.modal', function(event) {
var resTr = $(event.relatedTarget)
var wishid = resTr.data('wishid')
var reserved = resTr.data('reserved')
var modal = $(this)
modal.find('#modal-wishid').val(wishid)
modal.find('#modal-reservedstat').val(reserved)
if (reserved == 1) {
modal.find('#reservation-submit').text('Reservierung aufheben')
modal.find('#reservationModalLabel').text('Reservierung aufheben')
modal.find('#ReservationInfoText').remove()
} else {
modal.find('#reservation-submit').text('Reservieren')
modal.find('#reservationModalLabel').text('Wunsch reservieren')
}
});
<?php
if ($loggedin == true) {
echo ('
$(\'#deleteModal\').on(\'show.bs.modal\', function(event) {
var resTr = $(event.relatedTarget)
var whishcard = resTr.parents().closest(\'.card\');
var whishtitle = whishcard.find(\'.card-title\').text();
var wishid = resTr.data(\'wishid\')
var modal = $(this)
modal.find(\'#WhishID\').val(wishid)
modal.find(\'#whish-title\').text(whishtitle)
});
$(\'#pushprioModal\').on(\'show.bs.modal\', function(event) {
var resTr = $(event.relatedTarget)
var whishcard = resTr.parents().closest(\'.card\');
var whishtitle = whishcard.find(\'.card-title\').text();
var wishid = resTr.data(\'wishid\')
var modal = $(this)
modal.find(\'#WhishID\').val(wishid)
modal.find(\'#whish-title\').text(whishtitle)
});
');
}
?>
$(document).ready(function() {
$('#sortby').on('change', function() {
this.form.submit();
});
});
</script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/wishlist.js"></script>
</body>
</html>

426
item.php Normal file
View File

@@ -0,0 +1,426 @@
<?php
declare(strict_types=1);
/* ========= Session & Bootstrap ========= */
$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();
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';
}
function ip_in_cidr(string $ip, string $cidr): bool
{
if (strpos($cidr, ':') !== false) {
[$subnet, $mask] = array_pad(explode('/', $cidr, 2), 2, null);
$mask = (int) $mask;
$binIp = inet_pton($ip);
$binSubnet = inet_pton($subnet);
if ($binIp === false || $binSubnet === false)
return false;
$bytes = intdiv($mask, 8);
$bits = $mask % 8;
if ($bytes && substr($binIp, 0, $bytes) !== substr($binSubnet, 0, $bytes))
return false;
if ($bits) {
$b1 = ord($binIp[$bytes]) & (0xFF << (8 - $bits));
$b2 = ord($binSubnet[$bytes]) & (0xFF << (8 - $bits));
return $b1 === $b2;
}
return true;
} else {
[$subnet, $mask] = array_pad(explode('/', $cidr, 2), 2, null);
$mask = (int) $mask;
$ipL = ip2long($ip);
$subL = ip2long($subnet);
if ($ipL === false || $subL === false)
return false;
$maskL = -1 << (32 - $mask);
return (($ipL & $maskL) === ($subL & $maskL));
}
}
function is_private_ip(string $ip): bool
{
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
foreach (['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '169.254.0.0/16'] as $c)
if (ip_in_cidr($ip, $c))
return true;
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
foreach (['::1/128', 'fc00::/7', 'fe80::/10'] as $c)
if (ip_in_cidr($ip, $c))
return true;
}
return false;
}
function validate_remote_host_not_private(string $url): void
{
$p = parse_url($url);
if (!$p || empty($p['host']))
fail('Ungültige URL', 400);
$host = $p['host'];
global $image_host_whitelist;
if (isset($image_host_whitelist) && is_array($image_host_whitelist) && count($image_host_whitelist) > 0) {
$ok = false;
foreach ($image_host_whitelist as $allowed) {
if (strcasecmp($host, $allowed) === 0) {
$ok = true;
break;
}
if (preg_match('/\.' . preg_quote($allowed, '/') . '$/i', $host)) {
$ok = true;
break;
}
}
if (!$ok)
fail('Host nicht erlaubt', 400);
}
$recs = dns_get_record($host, DNS_A + DNS_AAAA);
if (!$recs || !count($recs))
fail('Host nicht auflösbar', 400);
foreach ($recs as $r) {
$ip = $r['type'] === 'A' ? ($r['ip'] ?? null) : ($r['ipv6'] ?? null);
if (!$ip)
continue;
if (is_private_ip($ip))
fail('Zieladresse unzulässig', 400);
}
}
function download_remote_image_limited(string $url, int $maxBytes = 5_000_000, int $timeout = 8): string
{
$tmp = tempnam(sys_get_temp_dir(), 'wlimg_');
if ($tmp === false)
fail('Temp-Datei Fehler', 500);
$fh = fopen($tmp, 'wb');
if ($fh === false) {
@unlink($tmp);
fail('Temp-Datei Fehler', 500);
}
$ch = curl_init($url);
if ($ch === false) {
fclose($fh);
@unlink($tmp);
fail('Download Fehler', 500);
}
$received = 0;
curl_setopt_array($ch, [
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_USERAGENT => 'wishlist/1.0',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_WRITEFUNCTION => function ($ch, $data) use (&$received, $maxBytes, $fh) {
$len = strlen($data);
$received += $len;
if ($received > $maxBytes)
return 0;
return fwrite($fh, $data);
}
]);
$ok = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fh);
if (!$ok || $code < 200 || $code >= 300) {
@unlink($tmp);
fail('Bild-Download fehlgeschlagen', 400);
}
return $tmp;
}
function safe_image_filename_from_url(string $url): string
{
$stripped = strtok($url, '?#');
$ext = strtolower(pathinfo((string) $stripped, PATHINFO_EXTENSION));
if (!preg_match('/^[a-z0-9]{1,5}$/i', $ext))
$ext = 'jpg';
return bin2hex(random_bytes(10)) . '.' . $ext;
}
/* ============= 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 !== '') {
if (!is_valid_http_url($ItemImageUrl)) {
$conn->close();
fail('Ungültiger Bildlink', 400);
}
validate_remote_host_not_private($ItemImageUrl);
$tmp = download_remote_image_limited($ItemImageUrl, 5_000_000, 8);
$info = @getimagesize($tmp);
if ($info === false || empty($info['mime']) || stripos($info['mime'], 'image/') !== 0) {
@unlink($tmp);
$conn->close();
fail('Link ist kein gültiges Bild', 400);
}
global $imagedir;
if (!is_dir($imagedir)) {
@mkdir($imagedir, 0755, true);
}
$filename = safe_image_filename_from_url($ItemImageUrl);
$target = rtrim($imagedir, '/') . '/' . $filename;
if (!@rename($tmp, $target)) {
// Fallback falls rename scheitert
if (!@copy($tmp, $target)) {
@unlink($tmp);
$conn->close();
fail('Bildspeicherung fehlgeschlagen', 500);
}
@unlink($tmp);
}
// HIER: Permissions fixen
@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;

151
js/wishlist.js Normal file
View File

@@ -0,0 +1,151 @@
// js/wishlist.js
(function () {
"use strict";
// Sort direkt submitten
const sortSel = document.getElementById("sortby");
if (sortSel && sortSel.form) {
sortSel.addEventListener("change", function () {
this.form.submit();
});
}
// Reservation Modal
const reservationModal = document.getElementById("reservationModal");
if (reservationModal) {
reservationModal.addEventListener("show.bs.modal", function (ev) {
const btn = ev.relatedTarget;
if (!btn) return;
const wishid = btn.getAttribute("data-wishid");
const reserved = btn.getAttribute("data-reserved");
reservationModal.querySelector("#modal-wishid").value = wishid || "";
reservationModal.querySelector("#modal-reservedstat").value =
reserved || "";
const submitBtn = reservationModal.querySelector("#reservation-submit");
const titleEl = reservationModal.querySelector("#reservationModalLabel");
const infoEl = reservationModal.querySelector("#ReservationInfoText");
if (reserved === "1") {
submitBtn.textContent = "Reservierung aufheben";
titleEl.textContent = "Reservierung aufheben";
if (infoEl) infoEl.style.display = "none";
} else {
submitBtn.textContent = "Reservieren";
titleEl.textContent = "Wunsch reservieren";
if (infoEl) infoEl.style.display = "";
}
});
}
// Delete Modal
const deleteModal = document.getElementById("deleteModal");
if (deleteModal) {
deleteModal.addEventListener("show.bs.modal", function (ev) {
const btn = ev.relatedTarget;
if (!btn) return;
const card = btn.closest(".card");
const title = card
? card.querySelector(".card-title")?.textContent?.trim() || ""
: "";
const wishid = btn.getAttribute("data-wishid") || "";
// robust: suche entweder #DelWhishID ODER #WhishID
const idInput = deleteModal.querySelector(
'#DelWhishID, #WhishID, input[name="WhishID"]'
);
const titleEl = deleteModal.querySelector(
"#del-whish-title, #whish-title"
);
if (idInput) idInput.value = wishid;
if (titleEl) titleEl.textContent = title || "Wunsch";
});
}
// Push Prio Modal
const prioModal = document.getElementById("pushprioModal");
if (prioModal) {
prioModal.addEventListener("show.bs.modal", function (ev) {
const btn = ev.relatedTarget;
if (!btn) return;
const card = btn.closest(".card");
const title = card
? card.querySelector(".card-title")?.textContent?.trim() || ""
: "";
const wishid = btn.getAttribute("data-wishid") || "";
const idInput = prioModal.querySelector(
'#PrioWhishID, #WhishID, input[name="WhishID"]'
);
const titleEl = prioModal.querySelector(
"#prio-whish-title, #whish-title"
);
if (idInput) idInput.value = wishid;
if (titleEl) titleEl.textContent = title || "Wunsch";
});
}
// Add/Edit Item Modal
const itemModal = document.getElementById("itemModal");
if (itemModal) {
itemModal.addEventListener("show.bs.modal", function (ev) {
const btn = ev.relatedTarget;
if (!btn) return;
const mode = btn.getAttribute("data-mode") || "add";
const titleEl = itemModal.querySelector("#itemModalTitle");
const actionEl = itemModal.querySelector("#ItemAction");
const submitEl = itemModal.querySelector("#ItemSubmitBtn");
const removeWrap = itemModal.querySelector("#RemoveImageWrap");
const removeChk = itemModal.querySelector("#RemoveImage");
// Felder
const fTitle = itemModal.querySelector("#ItemTitle");
const fDesc = itemModal.querySelector("#ItemDescription");
const fPrice = itemModal.querySelector("#ItemPrice");
const fLink = itemModal.querySelector("#ItemLink");
const fImg = itemModal.querySelector("#ItemImage");
const fId = itemModal.querySelector("#WhishID");
const fQty = itemModal.querySelector("#ItemQty"); // <-- NEU
if (mode === "edit") {
titleEl.textContent = "Wunsch bearbeiten";
actionEl.value = "edit";
submitEl.textContent = "Speichern";
if (removeWrap) removeWrap.style.display = "";
if (removeChk) removeChk.checked = false;
const wishid = btn.getAttribute("data-wishid") || "-1";
const dTitle = btn.getAttribute("data-title") || "";
const dDesc = btn.getAttribute("data-description") || "";
const dPrice = btn.getAttribute("data-price") || "";
const dLink = btn.getAttribute("data-link") || "";
const dQty = btn.getAttribute("data-qty") || "1"; // <-- NEU
if (fId) fId.value = wishid;
if (fTitle) fTitle.value = dTitle;
if (fDesc) fDesc.value = dDesc;
if (fPrice) fPrice.value = dPrice;
if (fLink) fLink.value = dLink;
if (fImg) fImg.value = "";
if (fQty) fQty.value = dQty; // <-- NEU
} else {
titleEl.textContent = "Wunsch hinzufügen";
actionEl.value = "add";
submitEl.textContent = "Hinzufügen";
if (fId) fId.value = "-1";
if (fTitle) fTitle.value = "";
if (fDesc) fDesc.value = "";
if (fPrice) fPrice.value = "";
if (fLink) fLink.value = "";
if (fImg) fImg.value = "";
if (fQty) fQty.value = "1"; // <-- NEU
if (removeWrap) removeWrap.style.display = "none";
if (removeChk) removeChk.checked = false;
}
});
}
})();

143
reservations.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/* ========= Session & Bootstrap ========= */
$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();
umask(0022); // neue Dateien: 0644, Ordner: 0755
require_once __DIR__ . '/config/config.php';
/* ===== Debug Toggle (wie item.php) ===== */
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, int $code=400): void {
http_response_code($code);
// kleine Flash-Message für index.php
$_SESSION['flash'] = ['msg'=>$msg,'type'=> ($code>=400?'danger':'success')];
safe_redirect_back();
}
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 require_csrf(): void {
$t = (string)($_POST['csrf'] ?? '');
if (empty($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $t)) {
fail('Ungültiges CSRF-Token', 403);
}
}
function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8'); }
function safe_redirect_back(): void {
$ref = (string)($_SERVER['HTTP_REFERER'] ?? '');
if ($ref === '' || stripos($ref, 'http') !== 0) {
$host = $_SERVER['HTTP_HOST'] ?? '';
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
header('Location: '.$scheme.'://'.$host.'/', true, 303);
} else {
header('Location: '.$ref, true, 303);
}
exit;
}
/* ============= Controller ============= */
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
fail('Methode nicht erlaubt', 405);
}
require_csrf();
$wishId = (int)($_POST['wishid'] ?? -1);
$pw = (string)($_POST['WishPassword'] ?? '');
$reservedstat = (int)($_POST['reservedstat'] ?? 0); // 0 = setzen, 1 = aufheben
if ($wishId <= 0) fail('Ungültige Wunsch-ID', 400);
if ($pw === '') fail('Passwort erforderlich', 400);
$conn = db();
/* --- qty des Wunsch + bestehende Reservierungen zählen --- */
$qty = 1;
$stmt = $conn->prepare('SELECT qty FROM wishes WHERE ID = ?');
$stmt->bind_param('i', $wishId);
$stmt->execute();
$res = $stmt->get_result();
if (!$res || !($row = $res->fetch_assoc())) {
$stmt->close(); $conn->close(); fail('Wunsch nicht gefunden', 404);
}
$qty = max(1, (int)$row['qty']);
$stmt->close();
/* Existierende Reservierungen zählen */
$cnt = 0;
$stmt = $conn->prepare('SELECT COUNT(*) AS c FROM wishes_reservations WHERE wish_id = ?');
$stmt->bind_param('i',$wishId);
$stmt->execute();
$res = $stmt->get_result();
if ($res && ($row = $res->fetch_assoc())) $cnt = (int)$row['c'];
$stmt->close();
/* --- Operationen --- */
if ($reservedstat === 0) {
// setzen
if ($cnt >= $qty) {
$conn->close();
fail('Für diesen Wunsch sind bereits alle Exemplare reserviert.', 409);
}
$hash = password_hash($pw, PASSWORD_BCRYPT);
$ins = $conn->prepare('INSERT INTO wishes_reservations (wish_id, pass_hash, created_at) VALUES (?, ?, NOW())');
$ins->bind_param('is', $wishId, $hash);
if (!$ins->execute()) {
$ins->close(); $conn->close();
fail('Reservierung fehlgeschlagen', 500);
}
$ins->close();
$conn->close();
$_SESSION['flash'] = ['msg'=>'Reservierung eingetragen','type'=>'success'];
safe_redirect_back();
} else {
// aufheben: passenden Hash suchen und genau einen Eintrag löschen
$sel = $conn->prepare('SELECT id, pass_hash FROM wishes_reservations WHERE wish_id = ?');
$sel->bind_param('i',$wishId);
$sel->execute();
$res = $sel->get_result();
$deleted = false;
while ($row = $res->fetch_assoc()) {
$rid = (int)$row['id'];
$rhash= (string)$row['pass_hash'];
if (password_verify($pw, $rhash)) {
$del = $conn->prepare('DELETE FROM wishes_reservations WHERE id = ? LIMIT 1');
$del->bind_param('i', $rid);
$deleted = $del->execute();
$del->close();
break;
}
}
$sel->close();
$conn->close();
if ($deleted) {
$_SESSION['flash'] = ['msg'=>'Reservierung aufgehoben','type'=>'success'];
} else {
$_SESSION['flash'] = ['msg'=>'Kein passender Reservierungseintrag gefunden (Passwort korrekt?)','type'=>'warning'];
}
safe_redirect_back();
}

Binary file not shown.

Binary file not shown.

BIN
webfonts/fa-solid-900.woff2 Normal file

Binary file not shown.

View File

@@ -37,19 +37,19 @@ CREATE TABLE `lists` (
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `whishes`
-- Tabellenstruktur für Tabelle `wishes`
--
CREATE TABLE `whishes` (
CREATE TABLE `wishes` (
`ID` int(11) NOT NULL,
`whislist` int(11) NOT NULL DEFAULT 0,
`wishlist` int(11) NOT NULL DEFAULT 0,
`title` varchar(128) NOT NULL,
`description` text NOT NULL,
`link` text NOT NULL,
`image` text NOT NULL,
`price` int(11) NOT NULL,
`reserved` tinyint(1) NOT NULL DEFAULT 0,
`reserved_pw` varchar(64) NOT NULL DEFAULT '',
`pass_hash` varchar(64) NOT NULL DEFAULT '',
`date` date NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -64,9 +64,9 @@ ALTER TABLE `lists`
ADD PRIMARY KEY (`ID`);
--
-- Indizes für die Tabelle `whishes`
-- Indizes für die Tabelle `wishes`
--
ALTER TABLE `whishes`
ALTER TABLE `wishes`
ADD PRIMARY KEY (`ID`);
--
@@ -80,9 +80,9 @@ ALTER TABLE `lists`
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `whishes`
-- AUTO_INCREMENT für Tabelle `wishes`
--
ALTER TABLE `whishes`
ALTER TABLE `wishes`
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;