Geizkragen/productAdder.php

565 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @file productAdder.php
* @brief Datei productAdder.php zur Anlage neuer Produkte im System.
*
* @details Diese Datei stellt ein Formular sowie die serverseitige Logik bereit,
* um neue Produkte in die Datenbank einzufügen. Dabei werden Kategorien,
* Marken und dynamische Attribute berücksichtigt. Ein Bild-Upload oder
* die Angabe einer Bild-URL sind ebenfalls möglich.
* Automatisch generierter Doxygen-Header wurde erweitert.
*
* @author Fabian
* @date 2026-04-04
*/
// product_add.php
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @section Zugriffskontrolle
* @brief Überprüfung der Administrator-Rechte.
*
* Falls der Benutzer nicht eingeloggt ist oder die Rolle 'ADMIN' nicht besitzt,
* wird der Zugriff mit einem HTTP-Status 403 (Forbidden) verweigert und eine
* Fehlermeldung ausgegeben.
*/
/* =======================
0) Zugriffskontrolle nur ADMIN
======================= */
if (empty($_SESSION['user_id']) || empty($_SESSION['user_roles']) || !in_array('ADMIN', $_SESSION['user_roles'], true)) {
http_response_code(403);
include 'header.php';
echo '<main class="auth"><section class="auth__grid"><div class="auth__card">';
echo '<h2 class="auth__title">Zugriff verweigert</h2>';
echo '<p>Du hast keine Berechtigung, Produkte hinzuzufügen.</p>';
echo '</div></section></main>';
include 'footer.php';
exit;
}
/**
* @section Kategorie-Auswahl
* @brief Ermittlung der ausgewählten Kategorie-ID.
*
* Die Kategorie-ID wird entweder aus den GET- oder POST-Parametern bezogen.
* Es wird sichergestellt, dass die ID aus Ziffern besteht (ctype_digit).
*/
/* =======================
1) Kategorie aus GET
======================= */
$categoryID = 0;
if (isset($_GET['categoryID']) && ctype_digit($_GET['categoryID'])) {
$categoryID = (int)$_GET['categoryID'];
} elseif (isset($_POST['categoryID']) && ctype_digit($_POST['categoryID'])) {
$categoryID = (int)$_POST['categoryID'];
}
/**
* @section Datenbankverbindung
* @brief Initialisierung der Datenbankverbindung.
*
* Nutzt die globale Funktion db_connect() aus bootstrap.php / db.php,
* um eine Verbindung zur MySQL-Datenbank herzustellen.
*/
/* =======================
2) DB-Verbindung
======================= */
$conn = db_connect();
/* =======================
2b) Produkt löschen
======================= */
$deleteMessage = '';
$deleteMessageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_product') {
$delProductID = (int)$_POST['product_id'];
$catIDFromForm = (int)$_POST['categoryID']; // to redirect back
if ($delProductID > 0) {
$delStmt = $conn->prepare("DELETE FROM products WHERE productID = ?");
$delStmt->bind_param("i", $delProductID);
if ($delStmt->execute()) {
$deleteMessage = 'Produkt erfolgreich gelöscht!';
$deleteMessageType = 'success';
} else {
$deleteMessage = 'Fehler beim Löschen des Produkts (möglicherweise sind noch Angebote oder Attribute verknüpft?).';
$deleteMessageType = 'error';
}
$delStmt->close();
}
}
/**
* @section Kategorien-Laden
* @brief Abrufen aller verfügbaren Kategorien.
*
* Die Kategorien werden für das Dropdown-Menü im Formular benötigt
* und alphabetisch sortiert geladen.
*/
/* =======================
3) Kategorien laden
======================= */
$categories = [];
$result = $conn->query("
SELECT categoryID, name
FROM categories
ORDER BY name
");
while ($row = $result->fetch_assoc()) {
$categories[] = $row;
}
/**
* @section Marken-Laden
* @brief Abrufen aller verfügbaren Marken.
*
* Lädt die Marken alphabetisch sortiert aus der Datenbank,
* damit sie bei der Produkterstellung ausgewählt werden können.
*/
/* =======================
3b) Marken laden
======================= */
$brands = [];
$result = $conn->query("
SELECT brandID, name
FROM brands
ORDER BY name
");
while ($row = $result->fetch_assoc()) {
$brands[] = $row;
}
/**
* @section Attribute-Laden
* @brief Abrufen der kategoriespezifischen Attribute.
*
* Falls eine Kategorie ausgewählt wurde ($categoryID > 0), werden
* die zugehörigen Attribute (inklusive Einheit und Datentyp) geladen.
*/
/* =======================
4) Attribute zur Kategorie
======================= */
$attributes = [];
if ($categoryID > 0) {
$stmt = $conn->prepare("
SELECT a.attributeID, a.name, a.unit, a.dataType
FROM categoryAttributes ca
JOIN attributes a ON a.attributeID = ca.attributeID
WHERE ca.categoryID = ?
ORDER BY a.name
");
$stmt->bind_param("i", $categoryID);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$attributes[] = $row;
}
}
/**
* @section Produkt-Speicherung
* @brief Verarbeitung des abgesendeten Formulars zum Speichern eines Produkts.
*
* Diese Sektion verarbeitet alle übergebenen Formulardaten (Modell, Beschreibung,
* Kategorie, Marke, Bild-Upload/URL, Attribute), validiert sie und fügt das
* neue Produkt in die Datenbank ein.
*/
/* =======================
5) Produkt speichern
======================= */
$saveError = null;
/** @var bool $debugMode Aktiviert den Debug-Modus für zusätzliche Fehlerausgaben, wenn ?debug=1 übergeben wird. */
$debugMode = isset($_GET['debug']) && $_GET['debug'] === '1';
$debugDetails = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
/** @var string $model Der Name bzw. das Modell des Produkts */
$model = trim($_POST['model']);
/** @var string|null $description Optionale Produktbeschreibung */
$description = $_POST['description'] ?? null;
$categoryID = (int)$_POST['categoryID'];
$brandID = (int)($_POST['brandID'] ?? 0);
/** @var string $imageUrl Optionale Bild-URL */
$imageUrl = trim((string)($_POST['imageUrl'] ?? ''));
/** @var array|null $imageFile Das hochgeladene Dateiobjekt aus $_FILES */
$imageFile = (isset($_FILES['productImage']) && is_array($_FILES['productImage'])) ? $_FILES['productImage'] : null;
/** @var bool $hasUpload Wahr, wenn eine Datei hochgeladen wurde und kein Fehler UPLOAD_ERR_NO_FILE vorliegt */
$hasUpload = $imageFile && isset($imageFile['error']) && (int)$imageFile['error'] !== UPLOAD_ERR_NO_FILE;
$uploadMime = null;
if ($debugMode) {
$debugDetails['post_categoryID'] = $_POST['categoryID'] ?? null;
$debugDetails['post_brandID'] = $_POST['brandID'] ?? null;
$debugDetails['post_model'] = $model;
$debugDetails['file_present'] = $imageFile !== null ? 'yes' : 'no';
$debugDetails['file_error'] = $imageFile['error'] ?? null;
$debugDetails['file_name'] = $imageFile['name'] ?? null;
$debugDetails['file_size'] = $imageFile['size'] ?? null;
$debugDetails['file_tmp'] = isset($imageFile['tmp_name']) ? (string)$imageFile['tmp_name'] : null;
$debugDetails['upload_max_filesize'] = ini_get('upload_max_filesize');
$debugDetails['post_max_size'] = ini_get('post_max_size');
}
// Validierung der Pflichtfelder und hochgeladenen Dateien
if ($categoryID <= 0) {
$saveError = 'Bitte eine Kategorie auswählen.';
} elseif ($brandID <= 0) {
$saveError = 'Bitte eine Marke auswählen.';
} elseif ($model === '') {
$saveError = 'Bitte ein Modell angeben.';
} elseif ($imageUrl !== '' && !filter_var($imageUrl, FILTER_VALIDATE_URL)) {
$saveError = 'Bitte eine gueltige Bild-URL eingeben.';
} elseif ($hasUpload) {
$fileError = (int)$imageFile['error'];
if ($fileError !== UPLOAD_ERR_OK) {
$saveError = 'Bild-Upload fehlgeschlagen (Code ' . $fileError . ').';
} else {
$tmp = isset($imageFile['tmp_name']) ? (string)$imageFile['tmp_name'] : '';
if ($tmp === '' || !is_uploaded_file($tmp)) {
$saveError = 'Upload-Datei ungueltig.';
} else {
$allowedMimeToExt = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
$mime = null;
$imageInfo = @getimagesize($tmp);
$imageType = (is_array($imageInfo) && isset($imageInfo[2])) ? (int)$imageInfo[2] : null;
if ($imageType === IMAGETYPE_PNG) {
$mime = 'image/png';
} elseif ($imageType === IMAGETYPE_JPEG) {
$mime = 'image/jpeg';
}
if (!$mime) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmp);
}
$fileExt = strtolower(pathinfo((string)($imageFile['name'] ?? ''), PATHINFO_EXTENSION));
if ($debugMode) {
$debugDetails['getimagesize_mime'] = $imageInfo['mime'] ?? null;
$debugDetails['getimagesize_type'] = $imageType;
$debugDetails['finfo_mime'] = $mime;
$debugDetails['file_ext'] = $fileExt;
}
if (!$mime || !isset($allowedMimeToExt[$mime])) {
if (in_array($fileExt, ['jpg', 'jpeg', 'png'], true)) {
$uploadMime = ($fileExt === 'png') ? 'image/png' : 'image/jpeg';
} else {
$saveError = 'Nur JPG oder PNG sind erlaubt. Erkannter Typ: ' . ($mime ?: 'unbekannt');
}
} else {
$uploadMime = $mime;
}
}
}
}
if ($saveError === null) {
/**
* @brief Produkt anlegen
* Speichert die grundlegenden Produktdaten in der Tabelle 'products'.
*/
// --- Produkt anlegen ---
$stmt = $conn->prepare("
INSERT INTO products (categoryID, brandID, model, description)
VALUES (?, ?, ?, ?)
");
if (!$stmt) {
$saveError = 'Datenbankfehler beim Anlegen des Produkts.';
} else {
$stmt->bind_param("iiss", $categoryID, $brandID, $model, $description);
$ok = $stmt->execute();
if (!$ok) {
error_log('Product insert failed: ' . $stmt->error);
$saveError = 'Produkt konnte nicht gespeichert werden (DB-Fehler).';
if ($debugMode) {
$debugDetails['db_error'] = $stmt->error;
}
} else {
/** @var int $productID Die ID des neu erstellten Produkts (Last Insert ID) */
$productID = $stmt->insert_id;
/** @var string|null $publicImagePath Der öffentliche Pfad zum gespeicherten Produktbild */
$publicImagePath = null;
if ($hasUpload) {
// Verarbeitung des hochgeladenen Bildes und Generierung des Zielpfades
$relativeTargetDir = 'assets/images/products';
$dirTargetDir = rtrim(__DIR__, "\\/") . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeTargetDir);
$documentRoot = isset($_SERVER['DOCUMENT_ROOT']) ? (string)$_SERVER['DOCUMENT_ROOT'] : '';
$docRootTrim = rtrim($documentRoot, "\\/");
$docTargetDir = ($docRootTrim !== '')
? $docRootTrim . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeTargetDir)
: '';
$targetDir = $dirTargetDir;
if ($docTargetDir !== '' && !is_dir($dirTargetDir) && is_dir($docTargetDir)) {
$targetDir = $docTargetDir;
}
if (!is_dir($targetDir) && !@mkdir($targetDir, 0755, true)) {
$saveError = 'Zielordner fuer Upload nicht verfuegbar.';
} elseif (!is_writable($targetDir)) {
$saveError = 'Zielordner ist nicht beschreibbar.';
} else {
$tmp = (string)$imageFile['tmp_name'];
$mime = $uploadMime;
if (!$mime) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmp);
}
$filename = $productID . '.png';
$targetPath = rtrim($targetDir, "\\/") . DIRECTORY_SEPARATOR . $filename;
$savedOk = false;
if ($mime === 'image/png') {
$savedOk = move_uploaded_file($tmp, $targetPath);
} elseif ($mime === 'image/jpeg') {
$sourceImage = @imagecreatefromjpeg($tmp);
if ($sourceImage !== false) {
$savedOk = imagepng($sourceImage, $targetPath);
imagedestroy($sourceImage);
}
}
if ($savedOk) {
$publicImagePath = $relativeTargetDir . '/' . $filename;
} else {
$saveError = 'Bild konnte nicht gespeichert werden.';
}
}
} elseif ($imageUrl !== '') {
$publicImagePath = $imageUrl;
}
if ($saveError === null && $publicImagePath !== null) {
/**
* @brief Bildpfad aktualisieren
* Aktualisiert den Datensatz des Produkts mit dem korrekten Bildpfad.
*/
$stmtImg = $conn->prepare("UPDATE products SET imagePath = ? WHERE productID = ?");
if ($stmtImg) {
$stmtImg->bind_param("si", $publicImagePath, $productID);
$stmtImg->execute();
}
}
/**
* @brief Dynamische Attribute speichern
* Iteriert über die übergebenen Attribute und speichert sie
* typgerecht in der Tabelle 'productAttributes'.
*/
// --- Attribute speichern ---
if (!empty($_POST['attributes'])) {
$stmtAttr = $conn->prepare("
INSERT INTO productAttributes
(productID, attributeID, valueString, valueNumber, valueBool)
VALUES (?, ?, ?, ?, ?)
");
foreach ($_POST['attributes'] as $attributeID => $value) {
if ($value === '' || $value === null) {
continue;
}
$valueString = null;
$valueNumber = null;
$valueBool = null;
if (is_numeric($value)) {
$valueNumber = $value;
} elseif ($value === '0' || $value === '1') {
$valueBool = (int)$value;
} else {
$valueString = trim($value);
}
$stmtAttr->bind_param(
"iisdi",
$productID,
$attributeID,
$valueString,
$valueNumber,
$valueBool
);
$stmtAttr->execute();
}
}
if ($saveError === null) {
// Nach erfolgreichem Speichern wird die Seite neu geladen (Post/Redirect/Get-Pattern)
header("Location: productAdder.php?categoryID=" . $categoryID);
exit;
}
}
}
}
}
// Inkludiert den Header-Bereich des HTML-Dokuments
include 'header.php';
?>
<main class="auth">
<section class="auth__grid">
<!-- Kategorie waehlen -->
<div class="auth__card">
<header class="auth__header">
<h2 class="auth__title">Kategorie wählen</h2>
</header>
<form method="get" class="auth__form">
<div class="auth__select__wrap">
<label class="auth__select__label" for="categoryID">Kategorie</label>
<select id="categoryID" name="categoryID" class="auth__select" onchange="this.form.submit()"
required>
<option value="">Kategorie wählen</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['categoryID'] ?>"
<?= $cat['categoryID'] === $categoryID ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
<!-- Produkt anlegen -->
<?php if ($categoryID > 0): ?>
<div class="auth__card">
<header class="auth__header">
<h2 class="auth__title">Produkt verwalten</h2>
</header>
<form method="post" class="auth__form" enctype="multipart/form-data">
<input type="hidden" name="categoryID" value="<?= $categoryID ?>">
<?php if ($saveError): ?>
<p class="auth__error"><?= htmlspecialchars($saveError) ?></p>
<?php endif; ?>
<?php if ($debugMode && !empty($debugDetails)): ?>
<pre class="auth__error"><?= htmlspecialchars(print_r($debugDetails, true)) ?></pre>
<?php endif; ?>
<label for="brandID">Marke</label>
<select id="brandID" name="brandID" class="auth__select" required>
<option value="">Marke wählen</option>
<?php foreach ($brands as $brand): ?>
<option value="<?= $brand['brandID'] ?>">
<?= htmlspecialchars($brand['name']) ?>
</option>
<?php endforeach; ?>
</select>
<label for="model">Modell</label>
<input id="model" type="text" name="model" class="auth__input" required>
<label for="description">Beschreibung</label>
<textarea id="description" name="description" class="auth__input"></textarea>
<label for="productImage">Produktbild hochladen (PNG oder JPG)</label>
<div class="auth__field">
<input id="productImage" type="file" name="productImage" accept="image/*">
<p class="auth__tip">Erlaubt: JPG/PNG. max. 20MB.</p>
</div>
<label for="imageUrl">oder Bild-URL</label>
<input id="imageUrl" type="url" name="imageUrl" class="auth__input" placeholder="https://...">
<h3 class="auth__title">Attribute</h3>
<?php foreach ($attributes as $attr): ?>
<label>
<?= htmlspecialchars($attr['name']) ?>
<?php if ($attr['unit']): ?>
(<?= htmlspecialchars($attr['unit']) ?>)
<?php endif; ?>
</label>
<input
type="<?= $attr['dataType'] === 'number' ? 'number' : 'text' ?>"
name="attributes[<?= $attr['attributeID'] ?>]"
class="auth__input"
>
<?php endforeach; ?>
<div class="auth__actions">
<button type="submit" name="saveProduct" class="auth__submit">
Produkt speichern
</button>
</div>
</form>
</div>
<?php endif; ?>
<!-- Produkt löschen -->
<div class="auth__card">
<header class="auth__header">
<h2 class="auth__title">Produkt löschen</h2>
</header>
<?php if ($deleteMessage): ?>
<div class="auth__message auth__message--<?= $deleteMessageType ?>">
<?= htmlspecialchars($deleteMessage) ?>
</div>
<?php endif; ?>
<form method="post" class="auth__form">
<input type="hidden" name="categoryID" value="<?= $categoryID ?>">
<input type="hidden" name="action" value="delete_product">
<div class="auth__select__wrap">
<label class="auth__select__label" for="product_id">Produkt</label>
<select id="product_id" name="product_id" class="auth__select" required>
<option value="">Produkt wählen</option>
<?php
// Produkte laden (gefiltert nach Kategorie falls ausgewählt, sonst alle)
$products = [];
$query = "SELECT productID, model FROM products ORDER BY model";
if ($categoryID > 0) {
$query = "SELECT productID, model FROM products WHERE categoryID = " . (int)$categoryID . " ORDER BY model";
}
$result = $conn->query($query);
while ($row = $result->fetch_assoc()) {
$products[] = $row;
}
foreach ($products as $product): ?>
<option value="<?= $product['productID'] ?>">
<?= htmlspecialchars($product['model']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="auth__actions">
<button type="submit" class="auth__submit" style="background-color: #ef4444; border-color: #ef4444;" onclick="return confirm('Möchtest du das Produkt wirklich löschen?');">
Produkt löschen
</button>
</div>
</form>
</div>
</section>
</main>
<?php
/**
* @brief Datenbankverbindung schließen und Footer inkludieren.
*/
$conn->close();
include 'footer.php';
?>