Files
intaleq_v2/app/Http/Controllers/UploadController.php
2026-04-24 15:12:12 +03:00

182 lines
6.3 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
*
* متحكم الرفع (Upload Controller)
*
* الغرض من الملف:
* التعامل مع رفع الملفات والصور (مثل صور الهوية، رخصة القيادة، الصور الشخصية، والملفات الصوتية).
*
* كيفية العمل:
* 1. يستقبل الملفات المرفوعة من التطبيق.
* 2. يقوم بتخزين الملفات في المجلدات المخصصة لها على الخادم مع تسميتها بشكل فريد.
* 3. يرجع رابط الملف المرفوع ليتم تخزينه في قاعدة البيانات.
*/
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->attributes->get('_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->attributes->get('_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,
};
}
}