validate([ 'kilometer' => 'required|numeric|min:0', 'pressure_high' => 'required|numeric|min:0', 'pressure_low' => 'nullable|numeric|min:0', 'cabin_temperature' => 'nullable|numeric', 'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()), 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()), 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()), 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()), 'precheck_notes' => 'nullable|string', 'front_image' => 'required|image|mimes:jpeg,png,jpg|max:20480', ]); $data = [ 'transaction_id' => $transaction->id, 'precheck_by' => auth()->id(), 'precheck_at' => now(), 'police_number' => $transaction->police_number, 'spk_number' => $transaction->spk, 'kilometer' => $request->kilometer, 'pressure_high' => $request->pressure_high, 'pressure_low' => $request->pressure_low, 'cabin_temperature' => $request->cabin_temperature, 'ac_condition' => $request->ac_condition, 'blower_condition' => $request->blower_condition, 'evaporator_condition' => $request->evaporator_condition, 'compressor_condition' => $request->compressor_condition, 'precheck_notes' => $request->precheck_notes, ]; // Handle file uploads $imageFields = [ 'front_image', 'cabin_temperature_image', 'ac_image', 'blower_image', 'evaporator_image' ]; foreach ($imageFields as $field) { if ($request->hasFile($field) && $request->file($field)->isValid()) { try { $file = $request->file($field); // Enhanced security validation if (!$this->isValidImageFile($file)) { return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]); } // Generate unique filename with transaction ID $filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension(); // Create directory path: transactions/{transaction_id}/precheck/ $directory = 'transactions/' . $transaction->id . '/precheck'; // Ensure base storage directory exists $this->ensureStorageDirectoryExists(); // Ensure transactions directory exists if (!Storage::disk('public')->exists('transactions')) { Storage::disk('public')->makeDirectory('transactions', 0755, true); Log::info('Created transactions directory'); } // Ensure transaction ID directory exists $transactionDir = 'transactions/' . $transaction->id; if (!Storage::disk('public')->exists($transactionDir)) { Storage::disk('public')->makeDirectory($transactionDir, 0755, true); Log::info('Created transaction directory: ' . $transactionDir); } // Ensure precheck directory exists if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory, 0755, true); Log::info('Created precheck directory: ' . $directory); } // Store file in organized directory structure $path = $file->storeAs($directory, $filename, 'public'); // Store file path $data[$field] = $path; // Store metadata $data[$field . '_metadata'] = [ 'original_name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime_type' => $file->getMimeType(), 'uploaded_at' => now()->toISOString(), 'transaction_id' => $transaction->id, 'filename' => $filename, ]; Log::info('File uploaded successfully: ' . $path); } catch (\Exception $e) { // Log error for debugging Log::error('File upload failed: ' . $e->getMessage(), [ 'field' => $field, 'file' => $file->getClientOriginalName(), 'transaction_id' => $transaction->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]); } } } try { Precheck::create($data); return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan'); } catch (\Exception $e) { Log::error('Precheck creation failed: ' . $e->getMessage()); return back()->withErrors(['error' => 'Gagal menyimpan data precheck. Silakan coba lagi.']); } } public function edit(Transaction $transaction, Precheck $precheck) { $acConditions = Precheck::getAcConditionOptions(); $blowerConditions = Precheck::getBlowerConditionOptions(); $evaporatorConditions = Precheck::getEvaporatorConditionOptions(); $compressorConditions = Precheck::getCompressorConditionOptions(); return view('transaction.prechecks.edit', compact( 'transaction', 'precheck', 'acConditions', 'blowerConditions', 'evaporatorConditions', 'compressorConditions' )); } public function update(Request $request, Transaction $transaction, Precheck $precheck) { $request->validate([ 'kilometer' => 'required|numeric|min:0', 'pressure_high' => 'required|numeric|min:0', 'pressure_low' => 'nullable|numeric|min:0', 'cabin_temperature' => 'nullable|numeric', 'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()), 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()), 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()), 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()), 'precheck_notes' => 'nullable|string', 'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', ]); $data = [ 'kilometer' => $request->kilometer, 'pressure_high' => $request->pressure_high, 'pressure_low' => $request->pressure_low, 'cabin_temperature' => $request->cabin_temperature, 'ac_condition' => $request->ac_condition, 'blower_condition' => $request->blower_condition, 'evaporator_condition' => $request->evaporator_condition, 'compressor_condition' => $request->compressor_condition, 'precheck_notes' => $request->precheck_notes, ]; // Handle file uploads with security validation $imageFields = [ 'front_image', 'cabin_temperature_image', 'ac_image', 'blower_image', 'evaporator_image' ]; foreach ($imageFields as $field) { if ($request->hasFile($field) && $request->file($field)->isValid()) { try { $file = $request->file($field); // Enhanced security validation if (!$this->isValidImageFile($file)) { return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]); } // Generate unique filename with transaction ID $filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension(); // Create directory path: transactions/{transaction_id}/precheck/ $directory = 'transactions/' . $transaction->id . '/precheck'; // Ensure base storage directory exists $this->ensureStorageDirectoryExists(); // Ensure transactions directory exists if (!Storage::disk('public')->exists('transactions')) { Storage::disk('public')->makeDirectory('transactions', 0755, true); Log::info('Created transactions directory'); } // Ensure transaction ID directory exists $transactionDir = 'transactions/' . $transaction->id; if (!Storage::disk('public')->exists($transactionDir)) { Storage::disk('public')->makeDirectory($transactionDir, 0755, true); Log::info('Created transaction directory: ' . $transactionDir); } // Ensure precheck directory exists if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory, 0755, true); Log::info('Created precheck directory: ' . $directory); } // Delete old file if exists if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) { Storage::disk('public')->delete($precheck->$field); } // Store file in organized directory structure $path = $file->storeAs($directory, $filename, 'public'); // Store file path $data[$field] = $path; // Store metadata $data[$field . '_metadata'] = [ 'original_name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime_type' => $file->getMimeType(), 'uploaded_at' => now()->toISOString(), 'transaction_id' => $transaction->id, 'filename' => $filename, ]; Log::info('File uploaded successfully: ' . $path); } catch (\Exception $e) { // Log error for debugging Log::error('File upload failed: ' . $e->getMessage(), [ 'field' => $field, 'file' => $file->getClientOriginalName(), 'transaction_id' => $transaction->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]); } } } try { $precheck->update($data); return redirect()->route('transaction')->with('success', 'Precheck berhasil diperbarui'); } catch (\Exception $e) { Log::error('Precheck update failed: ' . $e->getMessage()); return back()->withErrors(['error' => 'Gagal memperbarui data precheck. Silakan coba lagi.']); } } public function print($transaction_id) { try { $precheck = Precheck::where('transaction_id', $transaction_id)->firstOrFail(); return view('transaction.prechecks.print', compact('precheck')); } catch (\Exception $e) { Log::error('Error printing precheck: ' . $e->getMessage()); return back()->with('error', 'Gagal membuka halaman print precheck.'); } } /** * Ensure the base storage directory exists */ private function ensureStorageDirectoryExists() { $storagePath = storage_path('app/public'); if (!is_dir($storagePath)) { if (!mkdir($storagePath, 0755, true)) { Log::error('Failed to create storage directory: ' . $storagePath); throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.'); } Log::info('Created storage directory: ' . $storagePath); } // Check if directory is writable if (!is_writable($storagePath)) { Log::error('Storage directory is not writable: ' . $storagePath); throw new \Exception( 'Storage directory is not writable: ' . $storagePath . '. ' . 'Please run one of these commands from your project root: ' . '1) php fix_permissions.php ' . '2) chmod -R 775 storage/ ' . '3) mkdir -p storage/app/public/transactions/{transaction_id}/precheck' ); } // Check if we can create subdirectories $testDir = $storagePath . '/test_' . time(); if (!mkdir($testDir, 0755, true)) { Log::error('Cannot create subdirectories in storage: ' . $storagePath); throw new \Exception( 'Cannot create subdirectories in storage. ' . 'Please check permissions and run: php fix_permissions.php' ); } // Clean up test directory rmdir($testDir); Log::info('Storage directory is properly configured: ' . $storagePath); } /** * Enhanced security validation for image files to prevent RCE attacks * * @param \Illuminate\Http\UploadedFile $file * @return bool */ private function isValidImageFile($file) { try { // 1. Check file extension (whitelist approach) $allowedExtensions = ['jpg', 'jpeg', 'png']; $extension = strtolower($file->getClientOriginalExtension()); if (!in_array($extension, $allowedExtensions)) { Log::warning('Invalid file extension: ' . $extension, [ 'filename' => $file->getClientOriginalName(), 'user_id' => auth()->id() ]); return false; } // 2. Check MIME type $allowedMimeTypes = [ 'image/jpeg', 'image/jpg', 'image/png' ]; $mimeType = $file->getMimeType(); if (!in_array($mimeType, $allowedMimeTypes)) { Log::warning('Invalid MIME type: ' . $mimeType, [ 'filename' => $file->getClientOriginalName(), 'user_id' => auth()->id() ]); return false; } // 3. Verify file is actually an image using getimagesize $imageInfo = @getimagesize($file->getPathname()); if ($imageInfo === false) { Log::warning('File is not a valid image: ' . $file->getClientOriginalName(), [ 'user_id' => auth()->id() ]); return false; } // 4. Check image dimensions (prevent extremely large images) $maxWidth = 5000; $maxHeight = 5000; if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) { Log::warning('Image dimensions too large: ' . $imageInfo[0] . 'x' . $imageInfo[1], [ 'filename' => $file->getClientOriginalName(), 'user_id' => auth()->id() ]); return false; } // 5. Check file size (max 20MB) $maxSize = 20 * 1024 * 1024; // 20MB if ($file->getSize() > $maxSize) { Log::warning('File size too large: ' . $file->getSize(), [ 'filename' => $file->getClientOriginalName(), 'user_id' => auth()->id() ]); return false; } // 6. Check for suspicious content in filename $filename = $file->getClientOriginalName(); $suspiciousPatterns = [ ' $filename, 'user_id' => auth()->id() ]); return false; } } // 7. Additional security: Check file header (magic bytes) $handle = fopen($file->getPathname(), 'rb'); if ($handle) { $header = fread($handle, 8); fclose($handle); // Check for valid image headers $validHeaders = [ "\xFF\xD8\xFF", // JPEG "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", // PNG ]; $isValidHeader = false; foreach ($validHeaders as $validHeader) { if (substr($header, 0, strlen($validHeader)) === $validHeader) { $isValidHeader = true; break; } } if (!$isValidHeader) { Log::warning('Invalid file header detected', [ 'filename' => $filename, 'user_id' => auth()->id(), 'header' => bin2hex($header) ]); return false; } } Log::info('File validation passed', [ 'filename' => $filename, 'size' => $file->getSize(), 'mime_type' => $mimeType, 'user_id' => auth()->id() ]); return true; } catch (\Exception $e) { Log::error('File validation error: ' . $e->getMessage(), [ 'filename' => $file->getClientOriginalName(), 'user_id' => auth()->id(), 'error' => $e->getMessage() ]); return false; } } }