172 lines
5.6 KiB
PHP
172 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Upload Controller
|
|
* Replaces: uploadImage.php, uploadImagePortrate.php, uploadImageType.php, etc.
|
|
*
|
|
* Security improvements over V1:
|
|
* - MIME type + magic bytes validation (not just extension)
|
|
* - Randomized filenames (prevents path traversal)
|
|
* - Max file size enforcement
|
|
* - No directory traversal possible
|
|
*/
|
|
class UploadController extends Controller
|
|
{
|
|
private const ALLOWED_IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
|
|
private const ALLOWED_AUDIO_MIMES = ['audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/ogg'];
|
|
private const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
private const MAX_AUDIO_SIZE = 10 * 1024 * 1024; // 10MB
|
|
|
|
/** POST /v2/uploads/card-image */
|
|
public function cardImage(Request $request): JsonResponse
|
|
{
|
|
return $this->handleImageUpload($request, 'card_images', 'cards');
|
|
}
|
|
|
|
/** POST /v2/uploads/profile-image */
|
|
public function profileImage(Request $request): JsonResponse
|
|
{
|
|
return $this->handleImageUpload($request, 'imageProfileCaptain', 'profiles');
|
|
}
|
|
|
|
/** POST /v2/uploads/document */
|
|
public function document(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'image' => 'required|file',
|
|
'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back',
|
|
]);
|
|
|
|
return $this->handleImageUpload($request, 'driver_documents', 'documents');
|
|
}
|
|
|
|
/** POST /v2/uploads/id-front */
|
|
public function idFront(Request $request): JsonResponse
|
|
{
|
|
return $this->handleImageUpload($request, 'card_images', 'ids/front');
|
|
}
|
|
|
|
/** POST /v2/uploads/id-back */
|
|
public function idBack(Request $request): JsonResponse
|
|
{
|
|
return $this->handleImageUpload($request, 'card_images', 'ids/back');
|
|
}
|
|
|
|
/** POST /v2/uploads/audio */
|
|
public function audio(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['audio' => 'required|file']);
|
|
|
|
$file = $request->file('audio');
|
|
|
|
// Validate MIME
|
|
$mime = $file->getMimeType();
|
|
if (!in_array($mime, self::ALLOWED_AUDIO_MIMES)) {
|
|
return response()->json(['status' => 'failure', 'message' => 'Invalid audio format'], 400);
|
|
}
|
|
|
|
if ($file->getSize() > self::MAX_AUDIO_SIZE) {
|
|
return response()->json(['status' => 'failure', 'message' => 'File too large'], 400);
|
|
}
|
|
|
|
$userId = $request->input('_jwt_user_id');
|
|
$ext = $file->getClientOriginalExtension() ?: 'mp3';
|
|
$filename = 'audio_' . Str::random(24) . '.' . $ext;
|
|
|
|
$uploadPath = public_path('uploads/audio');
|
|
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
|
|
|
$file->move($uploadPath, $filename);
|
|
|
|
$link = config('intaleq.upload_base_url') . '/uploads/audio/' . $filename;
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => ['link' => $link, 'filename' => $filename],
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Core image upload handler
|
|
*/
|
|
private function handleImageUpload(Request $request, string $table, string $subDir): JsonResponse
|
|
{
|
|
$request->validate(['image' => 'required|file']);
|
|
|
|
$file = $request->file('image');
|
|
|
|
// Validate MIME type
|
|
$mime = $file->getMimeType();
|
|
if (!in_array($mime, self::ALLOWED_IMAGE_MIMES)) {
|
|
return response()->json(['status' => 'failure', 'message' => 'Invalid image format. Allowed: JPG, PNG, WebP'], 400);
|
|
}
|
|
|
|
// Validate file size
|
|
if ($file->getSize() > self::MAX_IMAGE_SIZE) {
|
|
return response()->json(['status' => 'failure', 'message' => 'File too large (max 5MB)'], 400);
|
|
}
|
|
|
|
// Validate magic bytes (defense in depth)
|
|
$firstBytes = file_get_contents($file->getRealPath(), false, null, 0, 4);
|
|
if (!$this->validateMagicBytes($firstBytes, $mime)) {
|
|
return response()->json(['status' => 'failure', 'message' => 'File content does not match type'], 400);
|
|
}
|
|
|
|
// Generate safe filename
|
|
$userId = $request->input('_jwt_user_id');
|
|
$ext = match ($mime) {
|
|
'image/jpeg' => 'jpg',
|
|
'image/png' => 'png',
|
|
'image/webp' => 'webp',
|
|
default => 'jpg',
|
|
};
|
|
$filename = $subDir . '_' . Str::random(24) . '.' . $ext;
|
|
|
|
$uploadPath = public_path('uploads/' . $subDir);
|
|
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
|
|
|
$file->move($uploadPath, $filename);
|
|
|
|
$link = config('intaleq.upload_base_url') . '/uploads/' . $subDir . '/' . $filename;
|
|
|
|
// Save to DB
|
|
$dbData = [
|
|
'driverID' => $userId,
|
|
'image_name' => $filename,
|
|
'link' => $link,
|
|
'upload_date' => now(),
|
|
];
|
|
|
|
if ($request->has('doc_type')) {
|
|
$dbData['doc_type'] = $request->input('doc_type');
|
|
}
|
|
|
|
DB::connection('ride')->table($table)->insert($dbData);
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => ['link' => $link, 'filename' => $filename],
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Validate file magic bytes match the declared MIME type
|
|
*/
|
|
private function validateMagicBytes(string $bytes, string $mime): bool
|
|
{
|
|
return match ($mime) {
|
|
'image/jpeg' => str_starts_with($bytes, "\xFF\xD8\xFF"),
|
|
'image/png' => str_starts_with($bytes, "\x89\x50\x4E\x47"),
|
|
'image/webp' => str_starts_with($bytes, "RIFF"),
|
|
default => false,
|
|
};
|
|
}
|
|
}
|