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(',', Postcheck::getAcConditionOptions()), 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()), 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()), 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()), 'postcheck_notes' => 'nullable|string', 'front_image' => 'required|image|mimes:jpeg,png,jpg|max:20480', ]); $data = [ 'transaction_id' => $transaction->id, 'postcheck_by' => auth()->id(), 'postcheck_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, 'postcheck_notes' => $request->postcheck_notes, ]; // Handle file uploads securely $imageFields = [ 'front_image', 'cabin_temperature_image', 'ac_image', 'blower_image', 'evaporator_image' ]; foreach ($imageFields as $field) { $storedPath = $this->processImageUpload($request, $field, $transaction); if ($storedPath) { $data[$field] = $storedPath; } } try { Postcheck::create($data); return redirect()->route('transaction')->with('success', 'Postcheck berhasil disimpan'); } catch (\Exception $e) { Log::error('Postcheck creation failed: ' . $e->getMessage()); return back()->withErrors(['error' => 'Gagal menyimpan data postcheck. Silakan coba lagi.']); } } public function edit(Transaction $transaction, Postcheck $postcheck) { $acConditions = Postcheck::getAcConditionOptions(); $blowerConditions = Postcheck::getBlowerConditionOptions(); $evaporatorConditions = Postcheck::getEvaporatorConditionOptions(); $compressorConditions = Postcheck::getCompressorConditionOptions(); return view('transaction.postchecks.edit', compact( 'transaction', 'postcheck', 'acConditions', 'blowerConditions', 'evaporatorConditions', 'compressorConditions' )); } public function update(Request $request, Transaction $transaction, Postcheck $postcheck) { $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(',', Postcheck::getAcConditionOptions()), 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()), 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()), 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', 'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()), 'postcheck_notes' => 'nullable|string', 'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480', ]); $updateData = [ '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, 'postcheck_notes' => $request->postcheck_notes, ]; $imageFields = [ 'front_image', 'cabin_temperature_image', 'ac_image', 'blower_image', 'evaporator_image' ]; foreach ($imageFields as $field) { $newPath = $this->processImageUpload($request, $field, $transaction); if ($newPath) { // delete old file if exists if ($postcheck->{$field}) { $this->deleteIfExists($postcheck->{$field}); } $updateData[$field] = $newPath; } } try { $postcheck->update($updateData); return redirect()->route('transaction')->with('success', 'Postcheck berhasil diperbarui'); } catch (\Exception $e) { Log::error('Postcheck update failed: ' . $e->getMessage()); return back()->withErrors(['error' => 'Gagal memperbarui data postcheck. Silakan coba lagi.']); } } public function print($transaction_id) { try { $postcheck = Postcheck::where('transaction_id', $transaction_id)->firstOrFail(); return view('transaction.postchecks.print', compact('postcheck')); } catch (\Exception $e) { Log::error('Error printing postcheck: ' . $e->getMessage()); return back()->with('error', 'Gagal membuka halaman print postcheck.'); } } /** * 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}/postcheck' ); } // 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); } /** * Securely process image upload to prevent RCE. * - Only allows jpeg and png * - Generates safe filename * - Validates actual image content using getimagesize */ private function processImageUpload(Request $request, string $field, Transaction $transaction): ?string { if (!($request->hasFile($field) && $request->file($field)->isValid())) { return null; } $file = $request->file($field); // Double-check mime type from PHP, disallow svg/gif $allowedMimes = ['image/jpeg' => 'jpg', 'image/png' => 'png']; $mime = $file->getMimeType(); if (!array_key_exists($mime, $allowedMimes)) { throw new \RuntimeException('Tipe file tidak diperbolehkan'); } // Verify it's a real image by reading dimensions $imageInfo = @getimagesize($file->getRealPath()); if ($imageInfo === false) { throw new \RuntimeException('File bukan gambar yang valid'); } // Prepare directory $directory = 'transactions/' . $transaction->id . '/postcheck'; $this->ensureStorageDirectoryExists(); if (!Storage::disk('public')->exists('transactions')) { Storage::disk('public')->makeDirectory('transactions', 0755, true); } if (!Storage::disk('public')->exists('transactions/' . $transaction->id)) { Storage::disk('public')->makeDirectory('transactions/' . $transaction->id, 0755, true); } if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory, 0755, true); } // Safe filename $ext = $allowedMimes[$mime]; $filename = time() . '_' . bin2hex(random_bytes(6)) . '_' . $transaction->id . '_' . $field . '.' . $ext; // Store $path = $file->storeAs($directory, $filename, 'public'); Log::info('Secure image stored', ['field' => $field, 'path' => $path]); return $path; } /** * Delete a file from public storage if it exists */ private function deleteIfExists(string $path): void { try { if ($path && Storage::disk('public')->exists($path)) { Storage::disk('public')->delete($path); } } catch (\Throwable $e) { Log::warning('Failed to delete old image', ['path' => $path, 'error' => $e->getMessage()]); } } }