Support Image-Upload and Paste-from-Clipboard

This commit is contained in:
2025-09-15 09:48:26 +02:00
parent 216110992f
commit 8daa183885
5 changed files with 846 additions and 219 deletions

213
item.php
View File

@@ -2,7 +2,6 @@
declare(strict_types=1);
require_once __DIR__ . '/include/image_fetch.php';
use WList\Net\ImageFetch;
/* ========= Session & Bootstrap ========= */
@@ -93,6 +92,116 @@ function is_valid_http_url(string $url): bool
return $s === 'http' || $s === 'https';
}
/* ===== Bild-Speicher-Helper ===== */
function ensure_imagedir(): string
{
global $imagedir;
if (!is_dir($imagedir))
@mkdir($imagedir, 0755, true);
return rtrim($imagedir, '/');
}
function save_image_from_tmp(string $tmp, ?string $sourceNameOrUrl = null): string
{
$info = @getimagesize($tmp);
if ($info === false || empty($info['mime']) || stripos($info['mime'], 'image/') !== 0) {
@unlink($tmp);
fail('Ungültige Bilddatei', 400);
}
$dir = ensure_imagedir();
// Dateiname: wenn URL vorhanden → davon die Ext, sonst aus MIME
if ($sourceNameOrUrl && is_valid_http_url($sourceNameOrUrl)) {
$name = ImageFetch::safeFileNameFromUrl($sourceNameOrUrl);
} else {
$ext = 'jpg';
$mime = strtolower($info['mime']);
if (str_contains($mime, 'png'))
$ext = 'png';
elseif (str_contains($mime, 'webp'))
$ext = 'webp';
elseif (str_contains($mime, 'gif'))
$ext = 'gif';
elseif (str_contains($mime, 'avif'))
$ext = 'avif';
elseif (str_contains($mime, 'jpeg'))
$ext = 'jpg';
$name = bin2hex(random_bytes(10)) . '.' . $ext;
}
$target = $dir . '/' . $name;
if (!@rename($tmp, $target)) {
if (!@copy($tmp, $target)) {
@unlink($tmp);
fail('Bildspeicherung fehlgeschlagen', 500);
}
@unlink($tmp);
}
@chmod($target, 0644);
return $name;
}
function save_image_from_upload(array $file): string
{
// Erwartet $_FILES['ItemImageFile']
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
fail('Datei-Upload fehlgeschlagen', 400);
}
if (!empty($file['error'])) {
fail('Datei-Upload Fehler (Code ' . (int) $file['error'] . ')', 400);
}
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > 8 * 1024 * 1024) {
fail('Datei ist zu groß (max. 8 MB)', 400);
}
$tmp = $file['tmp_name'];
// doppelt absichern: in separärer Temp-Datei lesen/schreiben (optional)
$probe = @getimagesize($tmp);
if ($probe === false || empty($probe['mime']) || stripos($probe['mime'], 'image/') !== 0) {
fail('Hochgeladene Datei ist kein gültiges Bild', 400);
}
// in einen eigenen tmp kopieren, um unify mit save_image_from_tmp zu haben
$tmp2 = tempnam(sys_get_temp_dir(), 'wlimg_');
if ($tmp2 === false)
fail('Temp-Datei Fehler', 500);
if (!@copy($tmp, $tmp2)) {
@unlink($tmp2);
fail('Temp-Datei Fehler', 500);
}
$origName = (string) ($file['name'] ?? '');
// Falls Originalname eine Extension hat, nutzen wir sie indirekt über getimagesize() (oben)
return save_image_from_tmp($tmp2, $origName !== '' ? $origName : null);
}
function save_image_from_dataurl(string $dataUrl, string $suggestedName = 'clipboard.png'): string
{
// data:image/png;base64,....
if (!preg_match('#^data:(image/[\w\-\+\.]+);base64,(.+)$#i', $dataUrl, $m)) {
fail('Zwischenablage-Daten ungültig', 400);
}
$mime = strtolower($m[1]);
$b64 = $m[2];
// Größenlimit grob prüfen (Base64 ist ~33% größer)
$rawLen = (int) (strlen($b64) * 3 / 4);
if ($rawLen > 8 * 1024 * 1024) {
fail('Zwischenablage-Bild ist zu groß (max. 8 MB)', 400);
}
$bin = base64_decode($b64, true);
if ($bin === false || strlen($bin) === 0) {
fail('Zwischenablage-Daten ungültig (Decode)', 400);
}
$tmp = tempnam(sys_get_temp_dir(), 'wlimg_');
if ($tmp === false)
fail('Temp-Datei Fehler', 500);
if (file_put_contents($tmp, $bin) === false) {
@unlink($tmp);
fail('Temp-Datei Fehler', 500);
}
// suggestedName nur als Hint finale Ext über getimagesize/MIME
return save_image_from_tmp($tmp, $suggestedName);
}
/* ============= Controller ============= */
require_csrf();
@@ -104,7 +213,12 @@ $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
// NEU: drei mögliche Bild-Inputs
$ItemImageUrl = trim((string) ($_POST['ItemImageUrl'] ?? ''));
$ItemImagePaste = (string) ($_POST['ItemImagePaste'] ?? '');
$ItemImagePasteName = trim((string) ($_POST['ItemImagePasteName'] ?? 'clipboard.png'));
$ListUUID = trim((string) ($_POST['ItemListUUID'] ?? ''));
$sortbyTransfer = (string) ($_POST['sortby_transfer'] ?? 'priority');
@@ -141,54 +255,52 @@ if ($ItemTitle === '')
if ($ItemLink !== '' && !is_valid_http_url($ItemLink))
fail('Ungültiger Angebotslink', 400);
/* Optional: Bild von externer URL holen */
/* ===== Bild verarbeiten: Upload → Paste → URL ===== */
$imageLocalLink = null;
if (!$removeImage && $ItemImageUrl !== '') {
$hasUpload = isset($_FILES['ItemImageFile']) && is_array($_FILES['ItemImageFile']) && !empty($_FILES['ItemImageFile']['name']);
$hasPaste = $ItemImagePaste !== '';
$hasUrl = $ItemImageUrl !== '';
$whitelist = $image_host_whitelist ?? null;
if (!$removeImage) {
if ($hasUpload) {
// 1) Datei-Upload
$imageLocalLink = save_image_from_upload($_FILES['ItemImageFile']);
$fetch = ImageFetch::download($ItemImageUrl, [
'max_bytes' => 8_000_000,
'timeout' => 12,
'connect_timeout' => 5,
'retries' => 4,
'retry_backoff_ms' => 300,
'whitelist_hosts' => $whitelist,
'ip_resolve_v4' => true,
'referer' => 'auto',
'log_prefix' => 'wishlist-img',
]);
} elseif ($hasPaste) {
// 2) Zwischenablage (Data-URL)
$imageLocalLink = save_image_from_dataurl($ItemImagePaste, $ItemImagePasteName);
if (!$fetch['ok']) {
error_log("wishlist image error: http=" . ($fetch['http_code'] ?? 0) . " curl=" . ($fetch['curl_err'] ?? '-') . " url=$ItemImageUrl");
$conn->close();
fail('Bild-Download fehlgeschlagen', 400);
}
$info = @getimagesize($fetch['tmp_path']);
$mime = $info['mime'] ?? $fetch['mime'] ?? 'image/*';
if (stripos($mime, 'image/') !== 0) {
@unlink($fetch['tmp_path']);
$conn->close();
fail('Link ist kein gültiges Bild', 400);
}
global $imagedir;
if (!is_dir($imagedir))
@mkdir($imagedir, 0755, true);
$filename = ImageFetch::safeFileNameFromUrl($ItemImageUrl);
$target = rtrim($imagedir, '/') . '/' . $filename;
if (!@rename($fetch['tmp_path'], $target)) {
if (!@copy($fetch['tmp_path'], $target)) {
@unlink($fetch['tmp_path']);
} elseif ($hasUrl) {
// 3) Bild-URL (Download-Pflicht, sonst Fehler)
if (!is_valid_http_url($ItemImageUrl)) {
$conn->close();
fail('Bildspeicherung fehlgeschlagen', 500);
fail('Ungültiger Bildlink', 400);
}
@unlink($fetch['tmp_path']);
$fetch = ImageFetch::download($ItemImageUrl, [
'page_url' => $ItemLink !== '' && is_valid_http_url($ItemLink) ? $ItemLink : null,
'max_bytes' => 8_000_000,
'timeout' => 12,
'connect_timeout' => 5,
'retries' => 5,
'retry_backoff_ms' => 300,
'whitelist_hosts' => $image_host_whitelist ?? null,
'referer' => 'auto', // Falls keine page_url, nimmt er Bild-Origin
'log_prefix' => 'wishlist-img',
'debug' => true,
'try_http_versions' => ['2', '1.1'],
'try_ip_resolve_combo' => ['v4', 'auto'],
'force_client_hints' => true,
]);
if (!$fetch['ok']) {
error_log("wishlist image error: http=" . ($fetch['http_code'] ?? 0) . " curl=" . ($fetch['curl_err'] ?? '-') . " url=$ItemImageUrl");
$conn->close();
fail('Bild-Download fehlgeschlagen. Bitte Bild manuell speichern/hochladen oder per Zwischenablage einfügen und erneut versuchen.', 400);
}
$imageLocalLink = save_image_from_tmp($fetch['tmp_path'], $ItemImageUrl);
}
@chmod($target, 0644);
$imageLocalLink = $filename;
}
/* ====== ADD ====== */
@@ -227,8 +339,9 @@ if ($action === 'add') {
fail('Speichern fehlgeschlagen', 500);
}
$stmt->close();
}
/* ====== EDIT ====== */ elseif ($action === 'edit') {
/* ====== EDIT ====== */
} elseif ($action === 'edit') {
if ($WhishID <= 0) {
$conn->close();
fail('Ungültige Item-ID', 400);
@@ -251,16 +364,14 @@ if ($action === 'add') {
$newImage = $oldImage;
if ($removeImage) {
if (!empty($oldImage)) {
global $imagedir;
$full = rtrim($imagedir, '/') . '/' . $oldImage;
$full = ensure_imagedir() . '/' . $oldImage;
if (is_file($full))
@unlink($full);
}
$newImage = '';
} elseif ($imageLocalLink !== null) {
if (!empty($oldImage)) {
global $imagedir;
$full = rtrim($imagedir, '/') . '/' . $oldImage;
$full = ensure_imagedir() . '/' . $oldImage;
if (is_file($full))
@unlink($full);
}
@@ -291,8 +402,8 @@ if ($action === 'add') {
fail('Update fehlgeschlagen', 500);
}
$stmt->close();
}
/* ====== Unbekannt ====== */ else {
} else {
$conn->close();
fail('Unbekannte Aktion', 400);
}