many improvements
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
dataconfig/config.php
|
# Wishlist secrets & local artifacts
|
||||||
config/config.php
|
config/config.php
|
||||||
|
data/
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.cache/
|
||||||
|
@@ -1,9 +1,45 @@
|
|||||||
<?php
|
<?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';
|
$servername = 'localhost';
|
||||||
$username = 'wishlist';
|
$username = 'wishlist';
|
||||||
$db = 'wishlist';
|
|
||||||
$password = 'yourcooldbpasshere';
|
$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';
|
$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
9
css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
267
css/tweaks.css
267
css/tweaks.css
@@ -1,80 +1,231 @@
|
|||||||
|
/* ============ Base / Vars ============ */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Comfortaa';
|
font-family: "Comfortaa";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: url(../fonts/comfortaa.woff2) format('woff2');
|
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;
|
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 {
|
/* Utility */
|
||||||
font-size: 1.125rem;
|
.object-fit-cover {
|
||||||
text-anchor: middle;
|
object-fit: cover;
|
||||||
-webkit-user-select: none;
|
}
|
||||||
-moz-user-select: none;
|
.object-fit-contain {
|
||||||
user-select: none;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* ============ Cards ============ */
|
||||||
.bd-placeholder-img-lg {
|
.card {
|
||||||
font-size: 3.5rem;
|
border-radius: var(--wl-card-radius);
|
||||||
}
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.b-example-divider {
|
.card .ratio {
|
||||||
height: 3rem;
|
--bs-aspect-ratio: 56.25%; /* 16:9 Bildfläche */
|
||||||
background-color: rgba(0, 0, 0, .1);
|
border-top-left-radius: var(--wl-card-radius);
|
||||||
border: solid rgba(0, 0, 0, .15);
|
border-top-right-radius: var(--wl-card-radius);
|
||||||
border-width: 1px 0;
|
background: #fff;
|
||||||
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.card-img-top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 15vw;
|
height: 100%;
|
||||||
object-fit: scale-down;
|
padding: 0;
|
||||||
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
|
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 {
|
.card-text {
|
||||||
min-height: 3em;
|
color: #333;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
min-height: 2.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
/* Status rechts */
|
||||||
text-overflow: ellipsis;
|
.status-right .fs-6 {
|
||||||
white-space: nowrap;
|
line-height: 1.2;
|
||||||
overflow: hidden;
|
}
|
||||||
|
.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
118
deploy.sh
Executable 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
BIN
img/no-image-katie.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
BIN
img/placeholders/no-image-katie-1.png
Normal file
BIN
img/placeholders/no-image-katie-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
BIN
img/placeholders/no-image-katie-2.png
Normal file
BIN
img/placeholders/no-image-katie-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
@@ -1,32 +1,116 @@
|
|||||||
<?php
|
<?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)
|
if ($conn->connect_error)
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
err('DB-Verbindung fehlgeschlagen');
|
||||||
|
|
||||||
$sql = 'SELECT image FROM whishes';
|
// Bildverzeichnis prüfen/härten
|
||||||
$result = $conn->query($sql);
|
$baseDir = realpath(__DIR__ . '/..');
|
||||||
|
if ($baseDir === false)
|
||||||
|
err('BaseDir nicht gefunden');
|
||||||
|
|
||||||
if ($result !== false && $result->num_rows > 0)
|
$imgDirCfg = rtrim((string) $GLOBALS['imagedir'], '/');
|
||||||
{
|
$imgDir = realpath(__DIR__ . '/../' . $imgDirCfg);
|
||||||
if ($rows = $result->fetch_all())
|
if ($imgDir === false)
|
||||||
{
|
err('imagedir nicht gefunden: ' . $imgDirCfg);
|
||||||
foreach ($dir as $fileinfo) {
|
|
||||||
if (!$fileinfo->isDot()) {
|
|
||||||
$filename = $fileinfo->getFilename();
|
|
||||||
|
|
||||||
if (!in_array($filename, $rows))
|
// Verhindere, dass außerhalb des Projekts gelöscht wird
|
||||||
{
|
if (strpos($imgDir, $baseDir) !== 0)
|
||||||
$deletepath = '../' . $imagedir . '/' . $filename;
|
err('imagedir liegt außerhalb des Projekts: ' . $imgDir);
|
||||||
unset($deletepath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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();
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
<?php
|
|
||||||
if(isset($_GET['pass'])) {
|
|
||||||
echo(password_hash($_GET['pass'], PASSWORD_DEFAULT));
|
|
||||||
}
|
|
@@ -1,162 +1,332 @@
|
|||||||
<?php
|
<?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
|
||||||
{
|
{
|
||||||
global $loggedin, $imagedir;
|
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
$formatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
|
}
|
||||||
|
/** Attribut-Helper (für data-*, value, title, …) */
|
||||||
if (strlen($ItemComment) == 0) {
|
if (!function_exists('attr')) {
|
||||||
$ItemComment = ' ';
|
function attr(string $s): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo ('
|
/** Pfad zum Platzhalterbild ermitteln */
|
||||||
<div class="col">
|
/** Pfad zum Platzhalterbild ermitteln */
|
||||||
<div class="card shadow-sm">
|
if (!function_exists('lg_default_image')) {
|
||||||
<div class="card-header">
|
function lg_default_image(): string
|
||||||
<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>');
|
|
||||||
|
|
||||||
if($loggedin == true)
|
|
||||||
{
|
{
|
||||||
echo('
|
$dir = $_SERVER['DOCUMENT_ROOT'] . '/' . ($GLOBALS['placeholders_imagedir'] ?? 'img/placeholders');
|
||||||
<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>'
|
// 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;
|
||||||
|
|
||||||
|
// 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, ',', '.') . ' €';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kommentar (2 Zeilen max., CSS macht den Clamp)
|
||||||
|
$commentPlain = trim((string) $ItemComment);
|
||||||
|
$commentHtml = ($commentPlain === '') ? ' ' : 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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo('
|
if ($ItemReserved > 0) {
|
||||||
</div>
|
$userBtns[] = sprintf(
|
||||||
<div class="d-inline">
|
'<button class="btn btn-outline-primary btn-pill justify-content-center"
|
||||||
<small class="text-muted text-end">' . date('d.m.y', strtotime($ItemDate)) . '</small>
|
aria-label="Reservierung aufheben"
|
||||||
</div>
|
data-bs-toggle="modal" data-bs-target="#reservationModal"
|
||||||
</div>
|
data-wishid="%d" data-reserved="1">
|
||||||
</div>
|
<i class="fa-solid fa-unlock"></i>
|
||||||
</div>
|
<span>Reservierung aufheben</span>
|
||||||
</div>
|
</button>',
|
||||||
');
|
$ListItemID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wishlistMainBuilder($ListID, $sortby)
|
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 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>
|
||||||
|
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
// 1) Listen-Metadaten holen
|
||||||
$conn = new mysqli($servername, $username, $password, $db);
|
$stmt = $conn->prepare('SELECT title, description FROM lists WHERE ID = ?');
|
||||||
|
$stmt->bind_param('i', $ListID);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
|
||||||
// Check connection
|
$listTitle = 'Unbekannte Liste';
|
||||||
if ($conn->connect_error) {
|
$listDesc = '';
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
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;
|
// 2) ORDER BY Whitelist
|
||||||
$result = $conn->query($sql);
|
$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 ('
|
// 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();
|
||||||
|
|
||||||
|
// 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.';
|
||||||
|
|
||||||
|
echo <<<HTML
|
||||||
<section class="py-5 text-center container">
|
<section class="py-5 text-center container">
|
||||||
<div class="row py-lg-5">
|
<div class="row py-lg-4">
|
||||||
<div class="col-lg-6 col-md-8 mx-auto">
|
<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>
|
||||||
|
|
||||||
if ($result !== false && $result->num_rows > 0) {
|
<div class="album py-3 bg-light">
|
||||||
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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
');
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ('
|
|
||||||
</div></div></section>
|
|
||||||
');
|
|
||||||
|
|
||||||
// End of Header Generator
|
|
||||||
|
|
||||||
echo ('
|
|
||||||
<div class="album py-5 bg-light">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||||
');
|
HTML;
|
||||||
|
|
||||||
$sort = 'id';
|
if ($items !== false && $items->num_rows > 0) {
|
||||||
|
while ($row = $items->fetch_assoc()) {
|
||||||
switch ($sortby) {
|
generateListItem(
|
||||||
case 'price_asc':
|
(int) $row['ID'],
|
||||||
$sort = 'price ASC';
|
$row['image'] !== null ? (string) $row['image'] : null,
|
||||||
break;
|
(string) $row['title'],
|
||||||
case 'price_desc':
|
$row['link'] !== null ? (string) $row['link'] : null,
|
||||||
$sort = 'price DESC';
|
(int) $row['price'],
|
||||||
break;
|
$row['description'] !== null ? (string) $row['description'] : null,
|
||||||
case 'date_desc':
|
(int) $row['reserved_count'], // echte Anzahl Reservierungen
|
||||||
$sort = 'date DESC';
|
$row['date'] !== null ? (string) $row['date'] : null,
|
||||||
break;
|
isset($row['qty']) ? (int) $row['qty'] : 1
|
||||||
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;
|
} else {
|
||||||
$result = $conn->query($sql);
|
echo '<div class="col"><div class="alert alert-info w-100">Keine Einträge vorhanden.</div></div>';
|
||||||
|
|
||||||
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']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo ('
|
echo <<<HTML
|
||||||
</div></div></div>
|
</div>
|
||||||
');
|
</div>
|
||||||
|
</div>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
}
|
}
|
||||||
|
704
index.php
704
index.php
@@ -1,217 +1,265 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
declare(strict_types=1);
|
||||||
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');
|
|
||||||
|
|
||||||
|
// ===== 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;
|
$ListID = -1;
|
||||||
$loggedin = false;
|
$ListUUID = '';
|
||||||
|
if (isset($_GET['list'])) {
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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';
|
$sortby = 'priority';
|
||||||
|
|
||||||
if (isset($_GET['list'])) {
|
// ===== Login-Status =====
|
||||||
$ListID = $_GET['list'];
|
$loggedin = (isset($_SESSION['listid']) && $ListID === (int) $_SESSION['listid']);
|
||||||
}
|
$GLOBALS['loggedin'] = $loggedin; // für listgenerator.php
|
||||||
|
|
||||||
if (isset($_POST['sortby'])) {
|
// ===== POST-Actions (mit CSRF) =====
|
||||||
$sortby = $_POST['sortby'];
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
} else if (isset($_POST['sortby_transfer'])) {
|
require_csrf();
|
||||||
$sortby = $_POST['sortby_transfer'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_SESSION['listid'])) {
|
|
||||||
if ($ListID == $_SESSION['listid']) {
|
|
||||||
$loggedin = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// LOGIN
|
||||||
if (isset($_POST['login'])) {
|
if (isset($_POST['login'])) {
|
||||||
$ListPassword = $_POST['ListPassword'];
|
$ListPassword = (string) ($_POST['ListPassword'] ?? '');
|
||||||
$ListID = $_POST['ListID'];
|
$ListIdFromForm = (int) ($_POST['ListID'] ?? -1);
|
||||||
$conn = new mysqli($servername, $username, $password, $db);
|
$c = db();
|
||||||
|
$s = $c->prepare('SELECT edit_pw, uuid FROM lists WHERE ID=?');
|
||||||
// Check connection
|
$s->bind_param('i', $ListIdFromForm);
|
||||||
if ($conn->connect_error) {
|
$s->execute();
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
$r = $s->get_result();
|
||||||
}
|
if ($r && ($row = $r->fetch_assoc())) {
|
||||||
|
if (password_verify($ListPassword, (string) $row['edit_pw'])) {
|
||||||
$sql = 'SELECT edit_pw FROM lists WHERE ID = ' . $ListID;
|
$_SESSION['listid'] = $ListIdFromForm;
|
||||||
$result = $conn->query($sql);
|
|
||||||
|
|
||||||
if ($result !== false && $result->num_rows > 0) {
|
|
||||||
if ($row = $result->fetch_assoc()) {
|
|
||||||
if (password_verify($ListPassword, $row['edit_pw'])) {
|
|
||||||
$_SESSION['listid'] = $ListID;
|
|
||||||
$loggedin = true;
|
$loggedin = true;
|
||||||
$message = array('msg' => 'Login erfolgreich', 'type' => 'success');
|
$ListUUID = (string) $row['uuid'];
|
||||||
} else {
|
$message = ['msg' => 'Login erfolgreich', 'type' => 'success'];
|
||||||
$message = array('msg' => 'Falsches Passwort', 'type' => 'warning');
|
} else
|
||||||
}
|
$message = ['msg' => 'Falsches Passwort', 'type' => 'warning'];
|
||||||
}
|
} else
|
||||||
}
|
$message = ['msg' => 'Liste nicht gefunden', 'type' => 'warning'];
|
||||||
$conn->close();
|
$s->close();
|
||||||
|
$c->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LISTE ANLEGEN
|
||||||
if (isset($_POST['listadd'])) {
|
if (isset($_POST['listadd'])) {
|
||||||
$listName = $_POST['listName'];
|
$listName = (string) ($_POST['listName'] ?? '');
|
||||||
$listPassword = password_hash($_POST['listPassword'], PASSWORD_DEFAULT);
|
$listPasswordRaw = (string) ($_POST['listPassword'] ?? '');
|
||||||
$listDescription = $_POST['listDescription'];
|
$listDescription = (string) ($_POST['listDescription'] ?? '');
|
||||||
$conn = new mysqli($servername, $username, $password, $db);
|
$listPassword = password_hash($listPasswordRaw, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
// Check connection
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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;
|
|
||||||
|
|
||||||
|
$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;
|
$_SESSION['listid'] = $last_id;
|
||||||
$loggedin = true;
|
$loggedin = true;
|
||||||
$actual_link = 'http://' . $_SERVER['HTTP_HOST'] . '/?list=' . $last_id;
|
$scheme = $secure ? 'https' : 'http';
|
||||||
header('Location: ' . $actual_link);
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||||
|
header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($uuid));
|
||||||
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$message = array('msg' => 'Error: ' . $sql . '<br>' . $conn->error, 'type' => 'error');
|
$message = ['msg' => 'Unerwarteter Fehler beim Anlegen', 'type' => 'danger'];
|
||||||
}
|
}
|
||||||
|
$s->close();
|
||||||
$conn->close();
|
$c->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOGOUT
|
||||||
if (isset($_POST['logout'])) {
|
if (isset($_POST['logout'])) {
|
||||||
session_destroy();
|
session_destroy();
|
||||||
$loggedin = false;
|
$loggedin = false;
|
||||||
$message = array('msg' => 'Logout erfolgreich', 'type' => 'success');
|
$message = ['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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PRIORITÄT PUSHEN
|
||||||
if (isset($_POST['pushprio'])) {
|
if (isset($_POST['pushprio'])) {
|
||||||
|
$wishId = (int) ($_POST['WhishID'] ?? -1);
|
||||||
$nextPriority = 0;
|
$c = db();
|
||||||
|
$s = $c->prepare('SELECT COALESCE(MAX(priority),0) AS maxprio FROM wishes WHERE wishlist=?');
|
||||||
$conn = new mysqli($servername, $username, $password, $db);
|
$s->bind_param('i', $ListID);
|
||||||
|
$s->execute();
|
||||||
// Check connection
|
$r = $s->get_result();
|
||||||
if ($conn->connect_error) {
|
$next = 1;
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = 'SELECT MAX( priority ) AS maxprio FROM whishes WHERE whislist = ' . $ListID . ';';
|
// LÖSCHEN (nur eingeloggt)
|
||||||
$result = $conn->query($stmt);
|
if (isset($_POST['delete']) && $loggedin === true) {
|
||||||
|
$WhishID = (int) ($_POST['WhishID'] ?? -1);
|
||||||
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'];
|
|
||||||
$WhishTitle = '';
|
$WhishTitle = '';
|
||||||
|
$c = db();
|
||||||
$conn = new mysqli($servername, $username, $password, $db);
|
$s = $c->prepare('SELECT image, title FROM wishes WHERE ID=?');
|
||||||
|
$s->bind_param('i', $WhishID);
|
||||||
// Check connection
|
$s->execute();
|
||||||
if ($conn->connect_error) {
|
$r = $s->get_result();
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
if ($r && ($row = $r->fetch_assoc())) {
|
||||||
}
|
$WhishTitle = (string) $row['title'];
|
||||||
|
$imageFile = (string) $row['image'];
|
||||||
$sql = 'SELECT image, title FROM whishes WHERE ID = ' . $WhishID;
|
if (!empty($imageFile)) {
|
||||||
$result = $conn->query($sql);
|
global $imagedir;
|
||||||
|
$full = rtrim($imagedir, '/') . '/' . $imageFile;
|
||||||
if ($result !== false && $result->num_rows > 0) {
|
if (is_file($full))
|
||||||
while ($row = $result->fetch_assoc()) {
|
@unlink($full);
|
||||||
unlink($imagedir . '/' . $row['image']);
|
|
||||||
$WhishTitle = $row['title'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$s->close();
|
||||||
$sql = 'DELETE FROM whishes WHERE ID = ' . $WhishID;
|
$d = $c->prepare('DELETE FROM wishes WHERE ID=?');
|
||||||
if ($conn->query($sql) === TRUE)
|
$d->bind_param('i', $WhishID);
|
||||||
$message = array('msg' => 'Wunsch <b>"' . $WhishTitle . '"</b> gelöscht', 'type' => 'success');
|
$message = $d->execute() ? ['msg' => 'Wunsch <b>"' . e($WhishTitle) . '"</b> gelöscht', 'type' => 'success'] : ['msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger'];
|
||||||
else
|
$d->close();
|
||||||
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
|
$c->close();
|
||||||
} else {
|
|
||||||
$message = array('msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Simple Wishlist</title>
|
<title>Simple Wishlist</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||||
<!--<link rel="stylesheet" href="css/custom.css">-->
|
|
||||||
<link rel="stylesheet" href="css/tweaks.css">
|
<link rel="stylesheet" href="css/tweaks.css">
|
||||||
<script src="js/bootstrap.bundle.min.js"></script>
|
<link rel="stylesheet" href="css/fontawesome.min.css"/>
|
||||||
<script src="js/jquery.min.js"></script>
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
|
<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="32x32" href="img/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.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="mask-icon" href="img/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<link rel="shortcut icon" href="img/favicon.ico">
|
<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">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<link rel="manifest" href="img/site.webmanifest">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -220,39 +268,36 @@ if (isset($_POST['delete']) && $loggedin == true) {
|
|||||||
<div class="navbar navbar-dark bg-dark shadow-sm">
|
<div class="navbar navbar-dark bg-dark shadow-sm">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="#" class="navbar-brand d-flex align-items-center">
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2 bi bi-gift"
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<strong>Simple Wishlist</strong>
|
<strong>Simple Wishlist</strong>
|
||||||
</a>
|
</a>
|
||||||
<div class="nav navbar-nav navbar-right">
|
<div class="nav navbar-nav navbar-right">
|
||||||
<div class="d-grid gap-2 d-flex">
|
<div class="d-grid gap-2 d-flex">
|
||||||
<?php
|
<?php if ($loggedin): ?>
|
||||||
if ($loggedin == true) {
|
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
|
||||||
echo ('
|
data-bs-target="#itemModal" data-mode="add" data-listuuid="<?= e($ListUUID) ?>"
|
||||||
<form class="form-inline" action="" method="POST">
|
data-sort="<?= e($sortby) ?>">Add Item</button>
|
||||||
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal" data-bs-target="#addItemModal">Add Item</button>
|
|
||||||
</form>
|
|
||||||
<form class="form-inline" action="" method="POST">
|
<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>
|
<button type="submit" class="btn btn-outline-secondary my-2 my-sm-0" name="logout">Logout</button>
|
||||||
</form>
|
</form>
|
||||||
');
|
<?php else: ?>
|
||||||
} else {
|
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
|
||||||
echo ('
|
data-bs-target="#loginModal">Login</button>
|
||||||
<form class="form-inline" action="" method="POST">
|
<?php endif; ?>
|
||||||
<button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal" data-bs-target="#loginModal">Login</button>
|
|
||||||
</form>
|
|
||||||
');
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<form class="form-inline" action="" method="POST">
|
<form class="form-inline" action="" method="POST">
|
||||||
|
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
|
||||||
<select class="form-control" name="sortby" id="sortby">
|
<select class="form-control" name="sortby" id="sortby">
|
||||||
<option <?php echo ($sortby == 'priority' ? 'selected="selected"' : ''); ?> value="priority">Priorität</option>
|
<option <?= $sortby === 'priority' ? 'selected' : '' ?> value="priority">Priorität</option>
|
||||||
<option <?php echo ($sortby == 'price_asc' ? 'selected="selected"' : ''); ?> value="price_asc">Preis aufsteigend</option>
|
<option <?= $sortby === 'price_asc' ? 'selected' : '' ?> value="price_asc">Preis aufsteigend</option>
|
||||||
<option <?php echo ($sortby == 'price_desc' ? 'selected="selected"' : ''); ?> value="price_desc">Preis absteigend</option>
|
<option <?= $sortby === 'price_desc' ? 'selected' : '' ?> value="price_desc">Preis absteigend</option>
|
||||||
<option <?php echo ($sortby == 'date_desc' ? 'selected="selected"' : ''); ?> value="date_desc">Datum, neu -> alt</option>
|
<option <?= $sortby === 'date_desc' ? 'selected' : '' ?> value="date_desc">Datum, neu → alt</option>
|
||||||
<option <?php echo ($sortby == 'date_asc' ? 'selected="selected"' : ''); ?> value="date_asc">Datum, alt -> neu</option>
|
<option <?= $sortby === 'date_asc' ? 'selected' : '' ?> value="date_asc">Datum, alt → neu</option>
|
||||||
<option <?php echo ($sortby == 'random' ? 'selected="selected"' : ''); ?> value="random">Zufall</option>
|
<option <?= $sortby === 'random' ? 'selected' : '' ?> value="random">Zufall</option>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,158 +306,141 @@ if (isset($_POST['delete']) && $loggedin == true) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main class="pb-4">
|
||||||
<?php
|
<?php if (isset($message)): ?>
|
||||||
if (isset($message)) {
|
<div class="alert alert-<?= e($message['type']) ?> alert-dismissible fade show m-3" role="alert">
|
||||||
echo ('
|
<?= $message['msg'] ?>
|
||||||
<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>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
');
|
<?php endif; ?>
|
||||||
}
|
|
||||||
wishlistMainBuilder($ListID, $sortby);
|
<?php wishlistMainBuilder($ListID, $sortby); ?>
|
||||||
?>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="text-muted py-5">
|
<footer class="text-muted py-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p class="float-end mb-1">
|
<p class="float-end mb-1"><a href="#">Back to top</a></p>
|
||||||
<a href="#">Back to top</a>
|
|
||||||
</p>
|
|
||||||
<p class="mb-1">Simple Wishlist © by Marcel Peterkau</p>
|
<p class="mb-1">Simple Wishlist © by Marcel Peterkau</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<?php
|
<?php if (!$loggedin): ?>
|
||||||
if ($loggedin == true) {
|
<!-- Login Modal -->
|
||||||
echo ('
|
<div class="modal fade" id="loginModal" tabindex="-1" aria-hidden="true">
|
||||||
<!-- Modal addItem-->
|
|
||||||
<div class="modal fade" id="addItemModal" tabindex="-1" aria-labelledby="addItemModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="addItemModalLabel">Add new Item</h5>
|
<h5 class="modal-title">Login</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<label for="ListPassword" class="form-label">Passwort</label>
|
<label for="ListPassword" class="form-label">Passwort</label>
|
||||||
<div class="input-group mb-3">
|
<input type="password" class="form-control" id="ListPassword" name="ListPassword" required>
|
||||||
<input type="password" class="form-control" id="ListPassword" name="ListPassword" rows="3" required>
|
<input type="hidden" id="ListID" name="ListID" value="<?= (int) $ListID ?>">
|
||||||
<input type="hidden" id="ListID" name="ListID" value="' . $ListID . '">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
|
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
<button type="submit" name="login" class="btn btn-primary">Login</button>
|
<button type="submit" name="login" class="btn btn-primary">Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
');
|
<?php endif; ?>
|
||||||
}
|
|
||||||
|
|
||||||
if ($loggedin == true) {
|
<!-- Add/Edit Modal (ein Modal für beides) -->
|
||||||
echo ('
|
<div class="modal fade" id="itemModal" tabindex="-1" aria-hidden="true">
|
||||||
<!-- Modal Delete-->
|
|
||||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="deleteModalLabel">Wunsch löschen</h5>
|
<h5 class="modal-title" id="itemModalTitle">Wunsch hinzufügen</h5><button type="button" class="btn-close"
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
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>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h5 id="whish-title">WunschTitel</h5>
|
<h5 id="del-whish-title">WunschTitel</h5>
|
||||||
<p>Soll dieser Wunsch wirklich gelöscht werden?</p>
|
<p>Soll dieser Wunsch wirklich gelöscht werden?</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<input type="hidden" id="WhishID" name="WhishID" value="-1">
|
<input type="hidden" name="WhishID" id="DelWhishID" value="-1">
|
||||||
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
|
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
|
||||||
<button type="submit" name="delete" class="btn btn-primary">Löschen</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- END OF Modal Delete-->
|
|
||||||
|
|
||||||
<!-- Modal PushPrio-->
|
<!-- Push Priority Modal -->
|
||||||
<div class="modal fade" id="pushprioModal" tabindex="-1" aria-labelledby="pushprioModalLabel" aria-hidden="true">
|
<div class="modal fade" id="pushprioModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="pushprioModalLabel">Wunschpriorität</h5>
|
<h5 class="modal-title">Wunschpriorität</h5><button type="button" class="btn-close"
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h5 id="whish-title">WunschTitel</h5>
|
<h5 id="prio-whish-title">WunschTitel</h5>
|
||||||
<p>Soll die Priorität dieses Wunsch ganz nach oben gesetzt werden?</p>
|
<p>Priorität ganz nach oben setzen?</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<input type="hidden" id="WhishID" name="WhishID" value="-1">
|
<input type="hidden" name="WhishID" id="PrioWhishID" value="-1">
|
||||||
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
|
<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="button" class="btn btn-secondary" data-bs-dismiss="modal">Nein</button>
|
||||||
<button type="submit" name="pushprio" class="btn btn-primary">Ja</button>
|
<button type="submit" name="pushprio" class="btn btn-primary">Ja</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -420,91 +448,39 @@ if (isset($_POST['delete']) && $loggedin == true) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- END OF Modal PushPrio-->
|
|
||||||
');
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Modal Reservation-->
|
<!-- Reservation Modal (öffentlich) -->
|
||||||
<div class="modal fade" id="reservationModal" tabindex="-1" aria-labelledby="reservationModalLabel" aria-hidden="true">
|
<div class="modal fade" id="reservationModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="reservationModalLabel">Wunsch reservieren</h5>
|
<h5 class="modal-title" id="reservationModalLabel">Wunsch reservieren</h5><button type="button"
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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>
|
<p id="ReservationInfoText">Bitte vergeben Sie ein Passwort, um diesen Wunsch zu reservieren. Nur mit diesem
|
||||||
<form action="" method="POST">
|
Passwort (oder durch den Listeneigentümer) kann die Reservierung wieder aufgehoben werden.</p>
|
||||||
<label for="ListPassword" class="form-label">Passwort</label>
|
<form action="reservations.php" method="POST" id="reservationForm">
|
||||||
<div class="input-group mb-3">
|
<label for="WishPassword" class="form-label">Passwort</label>
|
||||||
<input type="password" class="form-control" id="WishPassword" name="WishPassword" rows="3" required>
|
<input type="password" class="form-control" id="WishPassword" name="WishPassword" required>
|
||||||
<input type="hidden" name="wishid" id="modal-wishid" value="">
|
<input type="hidden" name="wishid" id="modal-wishid" value="">
|
||||||
<input type="hidden" name="reservedstat" id="modal-reservedstat" value="">
|
<input type="hidden" name="reservedstat" id="modal-reservedstat" value="">
|
||||||
<input type="hidden" id="sortby_transfer" name="sortby_transfer" value="' . $sortby . '">
|
<input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
|
||||||
</div>
|
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
|
||||||
</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>
|
</form>
|
||||||
</div>
|
</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>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
462
item.php
462
item.php
@@ -1,72 +1,426 @@
|
|||||||
<?php
|
<?php
|
||||||
ini_set('display_errors', 1);
|
declare(strict_types=1);
|
||||||
ini_set('display_startup_errors', 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);
|
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 {
|
} else {
|
||||||
echo "Link is Not an Image";
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#---
|
|
||||||
|
|
||||||
$ItemPriceCents = floatval(str_replace(',', '.', str_replace('.', '', $ItemPrice))) * 100;
|
/* ============= 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);
|
$conn = new mysqli($servername, $username, $password, $db);
|
||||||
|
if ($conn->connect_error)
|
||||||
// Check connection
|
fail('Interner Fehler (DB)', 500);
|
||||||
if ($conn->connect_error) {
|
$conn->set_charset('utf8mb4');
|
||||||
die('Connection failed: ' . $conn->connect_error);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = 'SELECT MAX( priority ) AS maxprio FROM whishes WHERE whislist = ' . $ListID . ';';
|
/* ============= Controller ============= */
|
||||||
$result = $conn->query($stmt);
|
|
||||||
|
|
||||||
while ($row = mysqli_fetch_array($result)) {
|
require_csrf();
|
||||||
$nextPriority = $row['maxprio'] + 1;
|
$sessionListId = require_logged_in();
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $conn->prepare('INSERT INTO whishes (title, description, link, image, price, whislist, priority) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
$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');
|
||||||
|
|
||||||
if (false === $stmt) {
|
$removeImage = isset($_POST['RemoveImage']) && ($_POST['RemoveImage'] === '1');
|
||||||
die('prepare() failed: ' . htmlspecialchars($conn->error));
|
$WhishID = isset($_POST['WhishID']) ? (int) $_POST['WhishID'] : 0; // bei edit
|
||||||
}
|
|
||||||
|
|
||||||
$rc = $stmt->bind_param('ssssiii', $ItemTitle, $ItemDescription, $ItemLink, $imageLocalLink, $ItemPriceCents, $ListID, $nextPriority);
|
if (!preg_match('/^[0-9a-fA-F-]{32,36}$/', $ListUUID))
|
||||||
if (false === $rc) {
|
fail('Liste nicht autorisiert', 403);
|
||||||
die('bind_param() failed: ' . htmlspecialchars($stmt->error));
|
|
||||||
}
|
|
||||||
|
|
||||||
$rc = $stmt->execute();
|
$conn = db();
|
||||||
if (false === $rc) {
|
|
||||||
die('execute() failed: ' . htmlspecialchars($stmt->error));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* 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();
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
|
fail('Liste nicht autorisiert', 403);
|
||||||
|
}
|
||||||
|
$ListID = (int) $row['ID'];
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
/* 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
151
js/wishlist.js
Normal 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
143
reservations.php
Normal 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();
|
||||||
|
}
|
BIN
webfonts/fa-brands-400.woff2
Normal file
BIN
webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
webfonts/fa-regular-400.woff2
Normal file
BIN
webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
webfonts/fa-solid-900.woff2
Normal file
BIN
webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
16
wishlist.sql
16
wishlist.sql
@@ -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,
|
`ID` int(11) NOT NULL,
|
||||||
`whislist` int(11) NOT NULL DEFAULT 0,
|
`wishlist` int(11) NOT NULL DEFAULT 0,
|
||||||
`title` varchar(128) NOT NULL,
|
`title` varchar(128) NOT NULL,
|
||||||
`description` text NOT NULL,
|
`description` text NOT NULL,
|
||||||
`link` text NOT NULL,
|
`link` text NOT NULL,
|
||||||
`image` text NOT NULL,
|
`image` text NOT NULL,
|
||||||
`price` int(11) NOT NULL,
|
`price` int(11) NOT NULL,
|
||||||
`reserved` tinyint(1) NOT NULL DEFAULT 0,
|
`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()
|
`date` date NOT NULL DEFAULT current_timestamp()
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
@@ -64,9 +64,9 @@ ALTER TABLE `lists`
|
|||||||
ADD PRIMARY KEY (`ID`);
|
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`);
|
ADD PRIMARY KEY (`ID`);
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -80,9 +80,9 @@ ALTER TABLE `lists`
|
|||||||
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
|
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;
|
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user