188 lines
7.6 KiB
PHP
188 lines
7.6 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Ad;
|
||
use App\Models\Setting;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Storage;
|
||
|
||
class AdController extends Controller
|
||
{
|
||
public function index()
|
||
{
|
||
$ads = Ad::orderByDesc('created_at')->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;
|
||
}
|
||
}
|