get(); $settings = [ 'vad_enabled' => Setting::get('vad_enabled', '0'), 'vad_freq_episodes' => Setting::get('vad_freq_episodes', 2), 'vad_freq_minutes' => Setting::get('vad_freq_minutes', 5), 'vad_upsell_percent' => Setting::get('vad_upsell_percent', 20), 'banner_ads_enabled' => Setting::get('banner_ads_enabled', '0'), ]; $stats = [ 'total_impressions' => $ads->sum('impressions'), 'total_clicks' => $ads->sum('clicks'), 'avg_ctr' => $ads->sum('impressions') > 0 ? round($ads->sum('clicks') / $ads->sum('impressions') * 100, 2) : 0, 'active_count' => $ads->where('is_active', true)->count(), ]; return view('admin.ads.index', compact('ads', 'settings', 'stats')); } public function store(Request $request) { $data = $this->validateAd($request); if ($request->hasFile('media_file')) { $data['file_path'] = $this->storeMedia($request->file('media_file')); } unset($data['media_file']); Ad::create($data); return back()->with('success', 'Reklam eklendi.'); } public function edit(Ad $ad) { return view('admin.ads.edit', compact('ad')); } public function update(Request $request, Ad $ad) { $data = $this->validateAd($request, $ad); if ($request->hasFile('media_file')) { $newPath = $this->storeMedia($request->file('media_file')); if ($ad->file_path) Storage::disk('public')->delete($ad->file_path); $data['file_path'] = $newPath; } unset($data['media_file']); $ad->update($data); return redirect()->route('admin.ads.index')->with('success', 'Reklam güncellendi.'); } public function destroy(Ad $ad) { if ($ad->file_path) Storage::disk('public')->delete($ad->file_path); $ad->delete(); return back()->with('success', 'Reklam silindi.'); } public function toggle(Ad $ad) { $ad->update(['is_active' => !$ad->is_active]); return back()->with('success', $ad->is_active ? 'Reklam aktifleştirildi.' : 'Reklam durduruldu.'); } public function saveSettings(Request $request) { Setting::set('vad_enabled', $request->boolean('vad_enabled') ? '1' : '0', 'ads'); Setting::set('vad_freq_episodes', max(1, (int) $request->input('vad_freq_episodes', 2)), 'ads'); Setting::set('vad_freq_minutes', max(1, (int) $request->input('vad_freq_minutes', 5)), 'ads'); Setting::set('vad_upsell_percent', min(100, max(0, (int) $request->input('vad_upsell_percent', 20))), 'ads'); Setting::set('banner_ads_enabled', $request->boolean('banner_ads_enabled') ? '1' : '0', 'ads'); return back()->with('success', 'Reklam ayarları kaydedildi.'); } private function validateAd(Request $request, ?Ad $existing = null): array { $type = $request->input('type', 'video'); // Sunucu upload limitini aşan dosya: PHP boş/bozuk upload gönderir. // Sessizce medyasız reklam kaydetmek yerine net hata ver. $this->guardUploadError($request); // Yüklenmiş dosya da dış URL de yoksa reklam gösterilemez (media_url null olur). // Düzenlemede mevcut dosya varsa yeniden yükleme zorunlu değil. $hasExisting = $existing?->file_path || $existing?->external_url; $needsMedia = !$request->hasFile('media_file') && !$hasExisting; $data = $request->validate([ 'name' => 'required|string|max:120', 'type' => 'required|in:video,banner', 'placement' => 'required|in:preroll,home_mid,home_bottom', 'media_file' => [ 'nullable', 'file', $type === 'video' ? 'mimes:mp4,m4v' : 'mimes:jpg,jpeg,png,webp,gif', $type === 'video' ? 'max:102400' : 'max:20480', // video 100MB, görsel/gif 20MB ], 'external_url' => [$needsMedia ? 'required' : 'nullable', 'nullable', 'url', 'max:2000'], 'click_url' => 'nullable|url|max:2000', 'skip_after' => 'required|integer|min:0|max:60', 'weight' => 'required|integer|min:1|max:100', 'is_active' => 'boolean', 'starts_at' => 'nullable|date', 'ends_at' => 'nullable|date|after:starts_at', ], [ 'external_url.required' => 'Bir medya dosyası yükleyin veya dış URL girin. ' . 'Dosya seçtiyseniz sunucu yükleme limitini aşmış olabilir (maks. ' . ini_get('upload_max_filesize') . ').', 'media_file.mimes' => $type === 'video' ? 'Video dosyası MP4 formatında olmalı.' : 'Görsel JPG, PNG, WebP veya GIF formatında olmalı.', 'media_file.max' => 'Dosya çok büyük.', ]); // Checkbox işaretli değilse request'te hiç gelmez — açıkça boolean'a çevir $data['is_active'] = $request->boolean('is_active'); return $data; } /** PHP upload hatalarını (limit aşımı, kısmi yükleme) net mesajla yüzeye çıkar. */ private function guardUploadError(Request $request): void { $file = $request->file('media_file'); if (!$file || $file->isValid()) { return; } $msg = match ($file->getError()) { UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Dosya sunucunun yükleme limitini aşıyor (maks. ' . ini_get('upload_max_filesize') . '). Daha küçük bir dosya seçin veya hosting limitini yükseltin.', UPLOAD_ERR_PARTIAL => 'Dosya yalnızca kısmen yüklendi, lütfen tekrar deneyin.', UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE => 'Sunucu dosyayı geçici klasöre yazamadı. Hosting sağlayıcınıza bildirin.', default => 'Dosya yüklenemedi (hata kodu: ' . $file->getError() . ').', }; throw \Illuminate\Validation\ValidationException::withMessages(['media_file' => $msg]); } /** Dosyayı public diske yaz ve tam yazıldığını doğrula. */ private function storeMedia(\Illuminate\Http\UploadedFile $file): string { // NOT: klasör adı bilerek nötr ('ads' değil) — adblocker /media/ads/ yolunu // ERR_BLOCKED_BY_CLIENT ile engelliyor. 'content' engellenmez. $expected = $file->getSize(); $path = $file->store('content', 'public'); if (!$path || !Storage::disk('public')->exists($path)) { throw \Illuminate\Validation\ValidationException::withMessages([ 'media_file' => 'Dosya sunucuya kaydedilemedi. storage/app/public klasörünün yazma izni olduğundan emin olun.', ]); } // Kısmi yazma (disk dolu / kesilen upload) sessizce bozuk reklam bırakmasın $written = Storage::disk('public')->size($path); if ($expected > 0 && $written !== $expected) { Storage::disk('public')->delete($path); throw \Illuminate\Validation\ValidationException::withMessages([ 'media_file' => "Dosya eksik yüklendi ({$written}/{$expected} byte). Tekrar deneyin.", ]); } return $path; } }