many improvements

This commit is contained in:
2025-08-18 20:39:36 +02:00
parent e0481df164
commit b9d360542d
20 changed files with 1848 additions and 656 deletions

View File

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

View File

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

View File

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