- Moved uploads from Lucee webroot to /opt/payfrit-api/uploads/ - Updated nginx on both dev and biz to alias /uploads/ to new path - Replaced luceeWebroot() with uploadsRoot() helper - Temp files now use /opt/payfrit-api/temp/ - No more /opt/lucee or /var/www/biz.payfrit.com references Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
5.2 KiB
PHP
152 lines
5.2 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
runAuth();
|
|
|
|
/**
|
|
* Upload Item Photo
|
|
*
|
|
* Multipart form: ItemID (int), photo (file)
|
|
* Creates thumbnail (128px square), medium (400px), and full (1200px max) versions.
|
|
*/
|
|
|
|
$itemId = (int) ($_POST['ItemID'] ?? 0);
|
|
|
|
if ($itemId <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_itemid', 'MESSAGE' => 'ItemID is required']);
|
|
}
|
|
|
|
if (!isset($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'no_file', 'MESSAGE' => 'No file was uploaded']);
|
|
}
|
|
|
|
$allowedExtensions = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'heic', 'heif'];
|
|
$ext = strtolower(pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowedExtensions)) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invalid_type', 'MESSAGE' => "Only image files are accepted (jpg, jpeg, gif, png, webp, heic). Got: $ext"]);
|
|
}
|
|
|
|
// Determine uploads directory
|
|
$itemsDir = uploadsRoot() . '/items';
|
|
if (!is_dir($itemsDir)) {
|
|
mkdir($itemsDir, 0755, true);
|
|
}
|
|
|
|
try {
|
|
$tmpFile = $_FILES['photo']['tmp_name'];
|
|
|
|
// Delete old photos and thumbnails
|
|
foreach (['jpg', 'jpeg', 'gif', 'png', 'webp'] as $oldExt) {
|
|
foreach (['', '_thumb', '_medium'] as $suffix) {
|
|
$oldFile = "$itemsDir/{$itemId}{$suffix}.{$oldExt}";
|
|
if (file_exists($oldFile)) {
|
|
@unlink($oldFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load image with GD
|
|
$srcImage = null;
|
|
$mime = mime_content_type($tmpFile);
|
|
switch ($mime) {
|
|
case 'image/jpeg': $srcImage = imagecreatefromjpeg($tmpFile); break;
|
|
case 'image/png': $srcImage = imagecreatefrompng($tmpFile); break;
|
|
case 'image/gif': $srcImage = imagecreatefromgif($tmpFile); break;
|
|
case 'image/webp': $srcImage = imagecreatefromwebp($tmpFile); break;
|
|
default:
|
|
// Try JPEG as fallback (HEIC converted by server)
|
|
$srcImage = @imagecreatefromjpeg($tmpFile);
|
|
}
|
|
|
|
if (!$srcImage) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invalid_image', 'MESSAGE' => 'Could not process image file']);
|
|
}
|
|
|
|
// Fix EXIF orientation for JPEG
|
|
if (function_exists('exif_read_data') && in_array($mime, ['image/jpeg', 'image/tiff'])) {
|
|
$exif = @exif_read_data($tmpFile);
|
|
if ($exif && isset($exif['Orientation'])) {
|
|
switch ($exif['Orientation']) {
|
|
case 3: $srcImage = imagerotate($srcImage, 180, 0); break;
|
|
case 6: $srcImage = imagerotate($srcImage, -90, 0); break;
|
|
case 8: $srcImage = imagerotate($srcImage, 90, 0); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$origW = imagesx($srcImage);
|
|
$origH = imagesy($srcImage);
|
|
|
|
// Helper: resize to fit within maxSize box
|
|
$resizeToFit = function($img, int $maxSize) {
|
|
$w = imagesx($img);
|
|
$h = imagesy($img);
|
|
if ($w <= $maxSize && $h <= $maxSize) return $img;
|
|
|
|
if ($w > $h) {
|
|
$newW = $maxSize;
|
|
$newH = (int) ($h * ($maxSize / $w));
|
|
} else {
|
|
$newH = $maxSize;
|
|
$newW = (int) ($w * ($maxSize / $h));
|
|
}
|
|
$resized = imagecreatetruecolor($newW, $newH);
|
|
imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h);
|
|
return $resized;
|
|
};
|
|
|
|
// Helper: create square center-crop thumbnail
|
|
$createSquareThumb = function($img, int $size) {
|
|
$w = imagesx($img);
|
|
$h = imagesy($img);
|
|
|
|
// Resize so smallest dimension equals size
|
|
if ($w > $h) {
|
|
$newH = $size;
|
|
$newW = (int) ($w * ($size / $h));
|
|
} else {
|
|
$newW = $size;
|
|
$newH = (int) ($h * ($size / $w));
|
|
}
|
|
$resized = imagecreatetruecolor($newW, $newH);
|
|
imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h);
|
|
|
|
// Center crop to square
|
|
$x = (int) (($newW - $size) / 2);
|
|
$y = (int) (($newH - $size) / 2);
|
|
$thumb = imagecreatetruecolor($size, $size);
|
|
imagecopy($thumb, $resized, 0, 0, $x, $y, $size, $size);
|
|
imagedestroy($resized);
|
|
return $thumb;
|
|
};
|
|
|
|
// Create thumbnail (128x128 square for retina)
|
|
$thumb = $createSquareThumb($srcImage, 128);
|
|
imagejpeg($thumb, "$itemsDir/{$itemId}_thumb.jpg", 85);
|
|
imagedestroy($thumb);
|
|
|
|
// Create medium (400px max)
|
|
$medium = $resizeToFit($srcImage, 400);
|
|
imagejpeg($medium, "$itemsDir/{$itemId}_medium.jpg", 85);
|
|
if ($medium !== $srcImage) imagedestroy($medium);
|
|
|
|
// Save full size (1200px max)
|
|
$full = $resizeToFit($srcImage, 1200);
|
|
imagejpeg($full, "$itemsDir/{$itemId}.jpg", 90);
|
|
if ($full !== $srcImage) imagedestroy($full);
|
|
|
|
imagedestroy($srcImage);
|
|
|
|
$cacheBuster = date('YmdHis');
|
|
|
|
jsonResponse([
|
|
'OK' => true,
|
|
'ERROR' => '',
|
|
'MESSAGE' => 'Photo uploaded successfully',
|
|
'IMAGEURL' => "/uploads/items/{$itemId}.jpg?v={$cacheBuster}",
|
|
'THUMBURL' => "/uploads/items/{$itemId}_thumb.jpg?v={$cacheBuster}",
|
|
'MEDIUMURL' => "/uploads/items/{$itemId}_medium.jpg?v={$cacheBuster}",
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage(), 'DETAIL' => '']);
|
|
}
|