A lot of things

This commit is contained in:
2026-06-16 16:21:43 +02:00
parent 4f9f6c63b3
commit 7e1e26f20b
126 changed files with 7917 additions and 204 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands;
use App\Models\Entry;
use App\Models\News;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
@@ -20,7 +21,11 @@ class DeleteRejectedEntries extends Command
$count = Entry::where('state', 'rejected')
->where('rejected_at', '<', now()->subDays($days))
->delete();
$count += News::where('state', 'rejected')
->where('rejected_at', '<', now()->subDays($days))
->delete();
$this->info("Deleted {$count} entries");
$this->info("Deleted {$count} entries/news");
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use App\Models\Entry;
#[Signature('entries:purge-featured {--days=15}')]
#[Description('Remove Featured from entries higher than X days')]
class PurgeFeaturedEntries extends Command
{
public function handle()
{
$days = $this->option('days');
$cutoff = now()->subDays($days);
$count = Entry::query()
->where('featured', true)
->where('featured_at', '<=', $cutoff)
->update([
'featured' => false,
'featured_at' => null,
]);
$this->info("$count entr" . ($count > 1 ? 'ies' : 'y') . " unfeatured.");
return self::SUCCESS;
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Helpers;
use App\Models\Entry;
use App\Models\EntryFile;
use App\Models\News;
use App\Services\XenforoApiService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
@@ -62,13 +64,16 @@ class EntryHelpers {
};
}
public static function getLatestComments(Entry $entry, int $limit = 20): array {
public static function getLatestComments(Entry|News $entry, int $limit = 20): array {
if( !$entry->comments_thread_id ){
return [];
}
$cacheKey = "entry_comments_{$entry->id}";
if( is_a( $entry, News::class ) )
$cacheKey = "news_comments_{$entry->id}";
else
$cacheKey = "entry_comments_{$entry->id}";
return Cache::remember($cacheKey, now()->addDays(1), function () use ($entry, $limit) {
$service = app(XenforoApiService::class);
@@ -93,6 +98,25 @@ class EntryHelpers {
public static function enableOnlinePatcherBasedOnExtension(string $filename): bool
{
return Str::endsWith(Str::lower($filename), ['.ips', '.bps', '.xdelta', '.zip' ]);
return Str::endsWith(Str::lower($filename), ['.ips', '.bps', '.xdelta', '.ups', '.aps', '.ppf', '.zip' ]);
}
public static function getYoutubeVideoId(string $url): ?string
{
$pattern = '%(?:https?://)?(?:www\.|m\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.com/(?:watch\?.*v=|embed/|v/|shorts/|live/))([\w-]{11})%i';
preg_match($pattern, $url, $matches);
return $matches[1] ?? null;
}
public static function fileAlreadyDownloaded(EntryFile $entryFile): bool
{
return session("downloaded_file_{$entryFile->file_uuid}", null ) !== null;
}
public static function markFileAsDownloaded(EntryFile $entryFile): bool
{
session(["downloaded_file_{$entryFile->file_uuid}" => 1]);
return true;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Helpers;
class PlayOnlineHelpers
{
public static function getCoreLists(): array
{
return [
'nes',
'fceumm',
'nestopia',
'snes',
'snes9x',
'bsnes',
'gb',
'gambatte',
'gba',
'mgba',
'nds',
'melonds',
'desmume2015',
'desmume',
'a5200',
'mame2003',
'mame2003_plus',
'fbneo',
'psx',
'pcsx_rearmed',
'mednafen_psx_hw',
'segaSaturn',
'yabuase',
'segaMD',
'segaGG',
'segaCD',
'genesis_plus_gx',
'n64',
'mupen64plus_next',
'parallel-n64',
'atari7800',
'prosystem',
'atari2600',
'stella2014',
'sega32x',
'picodrive',
'segaMS',
'smsplus',
'c64',
'vice_x64sc',
'same_cdi',
'psp',
'ppsspp',
'3ds',
'azahar'
];
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Helpers;
use App\Auth\XenForoUser;
use App\Models\Entry;
use App\Models\News;
use App\Services\XenforoApiService;
class XenForoHelpers {
@@ -44,7 +45,7 @@ class XenForoHelpers {
$service->updateEntriesCount( $count, $userId );
}
public static function entryApproved( Entry $entry ): void
public static function entryApproved( Entry|News $entry ): void
{
// 1. Update XF Entry count.
self::updateEntriesCount( $entry->user_id );
@@ -58,6 +59,29 @@ class XenForoHelpers {
$title = "Entry approved : {$entry->title}";
$message = "Your entry {$entry->title} has been approved by {$moderator}.";
$service = app(XenForoApiService::class);
$service->createConversation([ $entry->user_id ], $title, $message, false, false);
}
public static function entryRejected( Entry|News $entry ): void
{
// 1. Update XF Entry count.
self::updateEntriesCount( $entry->user_id );
// 2. Send a private message
/*
if( \Auth::user()->user_id === $entry->user_id ) {
return;
}
*/
$moderator = \Auth::user()->username;
$title = "Entry rejected : {$entry->title}";
$message = "Your entry {$entry->title} has been rejected by {$moderator}.\nReason: {$entry->staff_comment}\n\nYou have 7 days to edit your entry before it is permanently deleted.";
$service = app(XenForoApiService::class);
$service->createConversation([ $entry->user_id ], $title, $message, false, false);
}
}

View File

@@ -3,8 +3,10 @@
namespace App\Http\Controllers;
use App\Helpers\XenForoHelpers;
use App\Services\ActivityService;
use App\Services\XenforoApiService;
use App\Services\XenforoService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -63,4 +65,24 @@ class DynamicLoadController extends Controller
return response()->json( $data );
}
public function activityFeed(Request $request): JsonResponse
{
$availableFilters = ['entries', 'news', 'messages', 'threads', 'clubs'];
$requested = $request->query('filters')
? explode(',', $request->query('filters'))
: [];
$activeFilters = !empty($requested)
? array_intersect($requested, $availableFilters)
: $availableFilters;
$service = app(ActivityService::class);
$items = $service->getActivities(array_values($activeFilters));
return response()->json([
'html' => view('activity.timeline', compact('items'))->render(),
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\EntryHelpers;
use App\Models\Entry;
use App\Models\News;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -11,7 +12,7 @@ use Illuminate\View\View;
class EntryController extends Controller
{
private const SECTION_TYPES = ['translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts', 'tutorials'];
private const SECTION_TYPES = ['translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts'];
public function index(): View
{
@@ -31,7 +32,8 @@ class EntryController extends Controller
if ($entry->type !== $section)
abort(404);
Gate::authorize('viewAny', $entry);
if( !\Auth::guest() )
Gate::authorize('viewAny', $entry);
// Permissions.
$entryPolicy = match ($entry->state) {
@@ -61,7 +63,12 @@ class EntryController extends Controller
->orderBy('updated_at', 'desc')
->paginate(20);
return view('entries.drafts', compact('drafts'));
$newsDrafts = News::where('user_id', \Auth::user()->user_id )
->where('state', 'draft')
->orderBy('updated_at', 'desc')
->paginate(20);
return view('entries.drafts', compact('drafts', 'newsDrafts'));
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers;
use App\Models\Entry;
use App\Services\XenforoApiService;
use App\Services\XenforoService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EntryFeaturedRequestController extends Controller
{
public function featuredRequest(Request $request, Entry $entry)
{
$service = app(XenforoApiService::class);
$response = $service->featuredRequest($entry);
return response()->json([
'success' => $response,
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\EntryHelpers;
use App\Models\EntryFile;
use App\Models\LogXfUser;
use App\Services\FileServersService;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\JsonResponse;
@@ -45,7 +46,7 @@ class FileServerController extends Controller {
return response()->json($data);
}
\Cache::put("uploaded_file_{$fileUuid}", [
$fileData = [
'uuid' => $fileUuid,
'type' => $type,
'filename' => $filename,
@@ -54,7 +55,16 @@ class FileServerController extends Controller {
'favorite_server' => $data['favorite_server'],
'favorite_at' => time(),
'state' => 'public',
], now()->addHours(2) );
];
activity('entry-file')
->causedBy(LogXfUser::find(\Auth::user()->getAuthIdentifier()))
->withProperties($fileData)
->event('file_upload')
->log("File uploaded")
;
\Cache::put("uploaded_file_{$fileUuid}", $fileData, now()->addHours(2) );
$data['finished'] = true;
return response()->json($data);
@@ -67,7 +77,11 @@ class FileServerController extends Controller {
abort(404);
}
// TODO: DL Count.
if( !EntryHelpers::fileAlreadyDownloaded($file) ) {
EntryHelpers::markFileAsDownloaded($file);
$file->increaseDownloadCount();
}
return redirect( $this->fs->getDownloadFileUrl( $file) );
}
}

View File

@@ -2,14 +2,42 @@
namespace App\Http\Controllers;
use App\Models\Entry;
use App\Services\ActivityService;
use Illuminate\Http\Request;
use Illuminate\View\View;
use App\Models\News;
class HomeController extends Controller
{
public function index(): View {
return view('home');
public function __construct( private ActivityService $service ) {}
public function index( Request $request ): View {
$filters = [ 'entries', 'news', 'messages', 'threads', 'clubs' ];
$cookie = $request->cookie('activity_filters');
$activeFilters = $cookie ? array_intersect( json_decode( $cookie, true ) ?? [], $filters ) : $filters;
if( empty( $activeFilters ) ) {
$activeFilters = $filters;
}
$items = $this->service->getActivities( array_values( $activeFilters ) );
$viewFilters = [
'entries' => ['label' => 'Entries', 'icon' => 'database'],
'news' => ['label' => 'News', 'icon' => 'newspaper'],
'messages' => ['label' => 'Posts', 'icon' => 'message-square'],
'threads' => ['label' => 'Threads', 'icon' => 'messages-square'],
'clubs' => ['label' => 'Clubs', 'icon' => 'balloon'],
];
$latestNews = News::published()->latest('created_at')->limit(5)->get();
$featuredEntries = Entry::published()->where('featured', true)->latest('featured_at')->get();
return view('home', compact('items', 'activeFilters', 'viewFilters', 'latestNews', 'featuredEntries'));
}
}

View File

@@ -75,4 +75,9 @@ class ModCPController extends Controller
$entry->forceDelete();
return back()->with('success', "Entry permanently deleted");
}
public function logs()
{
return view('modcp.logs');
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\SubmissionException;
use App\Helpers\EntryHelpers;
use App\Http\Requests\StoreDraftRequest;
use App\Http\Requests\StoreEntryRequest;
use App\Http\Requests\StoreNewsDraftRequest;
use App\Http\Requests\StoreNewsRequest;
use App\Jobs\DeleteXenForoCommentsThread;
use App\Models\News;
use App\Services\NewsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class NewsController extends Controller
{
public function index()
{
return view('news.index');
}
public function show(Request $request, News $news)
{
if( !\Auth::guest() )
Gate::authorize('viewAny', $news);
// Permissions.
$entryPolicy = match ($news->state) {
'pending' => 'viewPending',
'draft' => 'viewDraft',
'rejected' => 'viewRejected',
'hidden' => 'viewHidden',
'locked' => 'viewLocked',
'published' => null,
'default' => null
};
if ($entryPolicy)
Gate::authorize($entryPolicy, $news);
$comments = EntryHelpers::getLatestComments($news);
return view('news.show', compact('news', 'comments'));
}
public function create(Request $request)
{
$data = [
'news' => new News(),
'isEdit' => false,
'oldCategory' => old('category') ? [ old('category') ] : []
];
return view ('news.create', $data);
}
public function edit(Request $request, News $news)
{
$data = [
'news' => $news,
'isEdit' => true,
'oldCategory' => old('category', $news->category_id) ? [ old('category', $news->category_id) ] : []
];
return view ('news.edit', $data);
}
public function store(Request $request)
{
$request = $request->input('submit-state') === 'draft' ? app(StoreNewsDraftRequest::class) : app(StoreNewsRequest::class);
$request->validateResolved();
$service = app(NewsService::class);
try {
$entry = $service->storeNews($request);
return match ($entry->state) {
'published' => redirect()->route('news.show', ['news' => $entry->slug])->with('success', "Your entry has been published."),
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
};
} catch ( SubmissionException $e ) {
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
} catch ( \Exception $e ) {
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
}
}
public function update(Request $request, News $news)
{
$request = $request->input('submit-state') === 'draft' ? app(StoreNewsDraftRequest::class) : app(StoreNewsRequest::class);
$request->validateResolved();
$service = app(NewsService::class);
try {
$news = $service->editNews($request, $news);
return match ($news->state) {
'published' => redirect()->route('news.show', ['news' => $news->slug])->with('success', "Your entry has been published."),
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
};
} catch ( SubmissionException $e ) {
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
} catch ( \Exception $e ) {
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
}
}
public function destroy(Request $request, News $news)
{
if( $news->comments_thread_id )
DeleteXenForoCommentsThread::dispatch( $news->comments_thread_id );
$news->delete();
return redirect( route('news.index') )->with('success', "Entry successfully deleted.");
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\XenForoHelpers;
use App\Models\Entry;
use App\Models\News;
use App\Services\XenforoService;
use Illuminate\Http\Request;
class QueueController extends Controller
@@ -14,7 +15,25 @@ class QueueController extends Controller
->with(['authors', 'game.platform'])
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
->orderBy('created_at', 'asc')
->get();
->get()
->map(fn($item) => $item->setAttribute('queue_type', 'entry'));
$news = News::inQueue()
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
->orderBy('created_at', 'asc')
->get()
->map(fn($item) => $item->setAttribute('queue_type', 'news'));
$entries = $entries->concat($news)->sort(function($a, $b) {
$aPending = $a->state === 'pending' ? 0 : 1;
$bPending = $b->state === 'pending' ? 0 : 1;
if($aPending !== $bPending) {
return $aPending <=> $bPending;
}
return $a->created_at <=> $b->created_at;
})->values();
return view('queue.index', compact('entries'));
}
@@ -25,24 +44,53 @@ class QueueController extends Controller
$entry->update(['staff_comment' => $request->input('comment')]);
return back()->with('success', 'Comment supdated');
}
public function updateComment_news(Request $request, News $news)
{
$request->validate(['comment' => 'nullable|string|max:2000']);
$news->update(['staff_comment' => $request->input('comment')]);
return back()->with('success', 'Comment updated');
}
public function approve(Request $request, Entry $entry)
{
// $entry->update(['state' => 'published']);
$entry->update(['state' => 'published', 'created_at' => now()]);
XenForoHelpers::entryApproved($entry);
return back()->with('success', 'Entry approved');
}
public function approve_news(Request $request, News $news)
{
$news->update(['state' => 'published', 'created_at' => now()]);
XenForoHelpers::entryApproved($news);
return back()->with('success', 'Entry approved');
}
public function reject(Request $request, Entry $entry)
{
$request->validate(['reason' => 'nullable|string|max:2000']);
$entry->update(['state' => 'rejected', 'staff_comment' => $request->input('reason'), 'rejected_at' => now() ]);
XenForoHelpers::entryRejected($entry);
return back()->with('success', 'Entry rejected');
}
public function reject_news(Request $request, News $news)
{
$request->validate(['reason' => 'nullable|string|max:2000']);
$news->update(['state' => 'rejected', 'staff_comment' => $request->input('reason'), 'rejected_at' => now() ]);
XenForoHelpers::entryRejected($news);
return back()->with('success', 'Entry rejected');
}

View File

@@ -14,4 +14,13 @@ class RedirectController extends Controller
return redirect()->route('entries.show', ['section' => $entry->type, 'entry' => $entry])->with('success', "Your report has been sent.");
}
public function newsReportRedirect( Request $request )
{
$id = $request->input('id');
$entry = News::findOrFail($id);
return redirect()->route('news.show', ['news' => $entry])->with('success', "Your report has been sent.");
}
}

View File

@@ -33,6 +33,62 @@ class SubmissionController extends Controller
public function __construct(private SubmissionsService $services){}
public function index(Request $request)
{
$entryTypes = [
'romhack' => [
'slug' => 'romhacks',
'label' => 'Romhack',
'icon' => 'gamepad-2',
'color' => '#4a6fc2',
'bg' => '#d5e1fc1a',
'border' => '#d5e1fc40',
],
'translation' => [
'slug' => 'translations',
'label' => 'Translation',
'icon' => 'languages',
'color' => '#4a8a2a',
'bg' => '#e7f4d91a',
'border' => '#e7f4d940',
],
'homebrew' => [
'slug' => 'homebrew',
'label' => 'Homebrew',
'icon' => 'cpu',
'color' => '#c23060',
'bg' => '#ffeaf01a',
'border' => '#ffeaf040',
],
'utility' => [
'slug' => 'utilities',
'label' => 'Utility',
'icon' => 'wrench',
'color' => '#8a6600',
'bg' => '#fff8d51a',
'border' => '#fff8d540',
],
'document' => [
'slug' => 'documents',
'label' => 'Document',
'icon' => 'file-text',
'color' => '#7a35c2',
'bg' => '#f3eaff1a',
'border' => '#f3eaff40',
],
'lua-script' => [
'slug' => 'lua-script',
'label' => 'Lua script',
'icon' => 'terminal',
'color' => '#a04515',
'bg' => '#eed6c51a',
'border' => '#eed6c540',
] ];
return view('submissions.index', compact('entryTypes'));
}
public function create(Request $request, string $section)
{
$data = [
@@ -108,7 +164,7 @@ class SubmissionController extends Controller
if( $entry->type !== $section )
abort(404);
if( $entry->comments_thread_id)
if( $entry->comments_thread_id )
DeleteXenForoCommentsThread::dispatch( $entry->comments_thread_id );
$entry->delete();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Entry;
use App\Models\EntryFile;
use App\Services\FileServersService;
use Illuminate\Http\Request;
@@ -15,9 +16,29 @@ class ToolsController extends Controller
return view('tools.patcher');
}
public function directPatch( Request $request, int $entry_id, EntryFile $file )
public function directPatch( Request $request, int $entryId, EntryFile $file )
{
if( $file->entry_id != $entry_id ) {
if( $file->entry_id != $entryId ) {
abort(404);
}
$service = app(FileServersService::class);
$patches = [
'file' => $service->getDownloadFileUrl( $file ),
'name' => $file->entry->title,
'outputName' => $file->filename
];
return view('tools.patcher', compact('patches'));
}
public function play( Request $request, int $entryId, EntryFile $file )
{
if( $file->entry_id != $entryId ) {
abort(404);
}
@@ -28,7 +49,11 @@ class ToolsController extends Controller
'name' => $file->entry->title,
'outputName' => $file->filename
];
$emuConfig = [
'core' => $file->playOnlineSetting?->core,
'threads' => $file->playOnlineSetting?->threads,
];
return view('tools.patcher', compact('patches'));
return view('tools.play', compact('patches', 'emuConfig'));
}
}

View File

@@ -149,6 +149,9 @@ class StoreEntryRequest extends FormRequest
$rules['files_metadata'] = 'array|nullable';
$rules['files_metadata.*.online_patcher'] = 'nullable|boolean';
$rules['files_metadata.*.secondary_online_patcher'] = 'nullable|boolean|required_with:files_metadata.*.online_patcher';
$rules['files_metadata.*.play_online'] = 'nullable|boolean';
$rules['files_metadata.*.play_online_core'] = 'nullable|string';
$rules['files_metadata.*.play_online_threads'] = 'nullable|boolean';
}
if( $isEdit && $this->user()->can('moderate', $this->route('entry') ) ){

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests;
class StoreNewsDraftRequest extends StoreNewsRequest
{
public function rules(): array
{
$rules = parent::rules();
$rules['submit-state'] = 'required|string|in:draft';
$rules = array_map(function($rule){
if( is_array($rule) ){
return array_map( fn($r) => $r === 'required' ? 'nullable' : $r, $rule);
}
return preg_replace(
['/\brequired_without\S*/', '/required_with\S*/', '/\brequired\b/'],
['nullable', 'nullable', 'nullable'],
$rule
);
}, $rules );
return $rules;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Requests;
use App\Models\News;
use App\Rules\PublicFileExists;
use App\Rules\XfUserExists;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreNewsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$news = $this->route('news');
if( $news )
return $this->user()->can('update', $news);
return $this->user()->can('create', News::class);
}
public function prepareForValidation(): void
{
$this->merge([
'gallery' => $this->input('gallery') !== '' ? $this->input('gallery') : null,
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$isEdit = (bool) $this->route('news');
$rules = [];
$rules['title'] = 'required|string|max:255';
$rules['category'] = 'required|integer|exists:categories,id';
$rules['description'] = 'required|string';
$rules['gallery'] = 'array|required|min:1';
$rules['gallery.*'] = [ 'string', new PublicFileExists ];
$rules['entry_id'] = 'nullable|integer|exists:entries,id';
$rules['release_site'] = 'nullable|url|max:500';
$rules['youtube_video'] = 'nullable|url|max:500';
if( $isEdit ){
$ss = 'draft,pending,published';
if( \Auth::user()->can('moderate', $this->route('news')) && \Auth::user()->can('view-hidden', $this->route('news')) )
$ss .= ',hidden';
if(\Auth::user()->can('moderate', $this->route('news')) && \Auth::user()->can('view-locked', $this->route('news')) )
$ss .= ',locked';
$rules['submit-state'] = 'required|in:' . $ss;
} else {
if( $this->user()->can('skip-queue', '\App\Models\News') ){
$rules['submit-state'] = 'required|string|in:draft,pending,published';
} else {
$rules['submit-state'] = 'required|string|in:draft,pending';
}
}
if( $isEdit && $this->user()->can('moderate', $this->route('news') ) ){
$rules['staff_comment'] = 'nullable|string';
$rules['owner_user_id'] = [ 'required', 'integer', new XfUserExists ];
$rules['comments_thread_id'] = 'nullable|integer';
}
return $rules;
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Entry;
use App\Models\News;
use App\Services\XenforoApiService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
@@ -18,7 +19,7 @@ class CreateXenForoCommentsThread implements ShouldQueue
* Create a new job instance.
*/
public function __construct(
protected Entry $entry
protected Entry|News $entry
)
{
//

36
app/Jobs/DeleteFile.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
namespace App\Jobs;
use App\Models\EntryFile;
use App\Services\FileServersService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class DeleteFile implements ShouldQueue
{
use Queueable;
public $tries = 3;
public $backoff = 10;
/**
* Create a new job instance.
*/
public function __construct(
public string $filePath,
public string $fileName,
public int $userId
)
{
//
}
/**
* Execute the job.
*/
public function handle(FileServersService $service): void
{
$service->deleteFile($this->filePath, $this->fileName, $this->userId);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Livewire;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\Activitylog\Models\Activity;
class ActivityLogs extends Component
{
use WithPagination;
#[Url(except: '')]
public string $search = '';
#[Url(except: '')]
public string $logName = '';
#[Url(except: '')]
public string $event = '';
#[Url(except: '')]
public string $dateFrom = '';
#[Url(except: '')]
public string $dateTo = '';
#[Url(except: '')]
public string $causerId = '';
public function updating(): void { $this->resetPage(); }
public function clearFilters(): void
{
$this->reset('search', 'logName', 'event', 'dateFrom', 'dateTo', 'causerId');
$this->resetPage();
}
public function render()
{
$logs = Activity::query()
->with(['causer'])
->when($this->search, fn($q) => $q
->where('description', 'like', "%{$this->search}%")
->orWhere('subject_type', 'like', "%{$this->search}%")
->orWhere('log_name', 'like', "%{$this->search}%")
)
->when($this->logName, fn($q) => $q->where('log_name', $this->logName))
->when($this->event, fn($q) => $q->where('event', $this->event))
->when($this->causerId, fn($q) => $q->where('causer_id', $this->causerId))
->when($this->dateFrom, fn($q) => $q->whereDate('created_at', '>=', $this->dateFrom))
->when($this->dateTo, fn($q) => $q->whereDate('created_at', '<=', $this->dateTo))
->latest()
->paginate(50);
$logNames = Activity::distinct()->orderBy('log_name')->pluck('log_name')->filter()->values();
$events = Activity::distinct()->orderBy('event')->pluck('event')->filter()->values();
$hasFilters = $this->logName || $this->event || $this->dateFrom || $this->dateTo || $this->causerId;
return view('livewire.activity-logs', compact('logs', 'logNames', 'events', 'hasFilters'));
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Livewire;
use App\Models\Entry;
use Livewire\Component;
class EntrySelector extends Component
{
public string $search = '';
public ?int $selectedEntryId = null;
public ?string $entryName = null;
public bool $dropdown = false;
public function mount( ?int $oldEntryId = null ): void
{
if( $oldEntryId ) {
$entry = Entry::find($oldEntryId);
if( $entry ) {
$this->selectedEntryId = $oldEntryId;
$this->entryName = $entry->complete_title ?? $entry->title;
}
}
}
public function updatedSearch(): void
{
if( $this->selectedEntryId ) {
$this->selectedEntryId = null;
$this->entryName = null;
}
$this->dropdown = strlen($this->search) > 2;
}
public function selectEntry( int $id ){
$entry = Entry::find($id);
if( $entry ) {
$this->selectedEntryId = $id;
$this->entryName = $entry->complete_title ?? $entry->title;
$this->search = $entry->complete_title ?? $entry->title;
$this->dropdown = false;
}
}
public function clearEntry(): void
{
$this->selectedEntryId = null;
$this->entryName = null;
$this->search = '';
}
public function render()
{
$entries = collect();
if( $this->dropdown && strlen($this->search) > 2 ) {
$entries = Entry::where('complete_title', 'like', '%' . $this->search . '%')
->orWhere('title', 'like', '%' . $this->search . '%')
->orderBy('complete_title', 'asc')
->orderBy('title', 'asc')
->limit(20)
->get();
}
$data = [ 'entries' => $entries, 'required_chars' => 3 ];
return view('livewire.entry-selector', $data);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Livewire;
use App\Models\Category;
use App\Models\News;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class NewsDatabase extends Component
{
use WithPagination;
/**
* News title search
* @var string
*/
#[Url(as: 's', except: '')]
public string $search = '';
/**
* Categories IDs filter.
* @var array
*/
#[Url(except:[])]
public array $categories = [];
/**
* Sort by field.
* @var string
*/
#[Url(as: 'sort',except: 'created_at')]
public string $sortBy = 'created_at';
/**
* asc/desc
* @var string
*/
#[Url(as: 'dir',except: 'desc')]
public string $sortDir = 'desc';
/**
* Translation of sort options key.
*/
public const array SORT_OPTIONS = [
'created_at' => 'Date added',
'title' => 'Title'
];
public const int PAGINATION = 30;
public function updatedSearch(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
public function updatedCategories(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
public function updatedCategoriesMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
public function clearFilters(): void
{
$this->reset([
'search', 'categories',
]);
$this->resetPage();
}
public function setSort(string $field): void
{
if( $this->sortBy === $field ) {
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDir = 'asc';
}
$this->resetPage();
$this->dispatch('filters-updated');
}
private function buildQuery()
{
$query = News::query()->published();
if( $this->search ){
$query->where(function($q){
$q->where('title', 'like', '%'.$this->search.'%');
});
}
if( $this->categories ) {
$query->whereIn('category_id', $this->categories);
}
return $query->orderBy($this->sortBy, $this->sortDir);
}
public function render()
{
return view('livewire.news-database', [
'news' => $this->buildQuery()->paginate(self::PAGINATION),
'allCategories' => Category::where(function ($query) {
$query->whereJsonContains('restricted_to', "news")
->orWhereNull('restricted_to');
})->orderBy('name')->get()
]);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Helpers\EntryHelpers;
use App\Traits\HasGallery;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -10,6 +11,8 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Monolog\Level;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions;
/**
* @property int $id
@@ -22,6 +25,7 @@ use Monolog\Level;
* @property string|null $staff_comment
* @property \Illuminate\Support\Carbon|null $rejected_at
* @property bool $featured
* @property \Illuminate\Support\Carbon|null $featured_at
* @property int|null $game_id
* @property int|null $platform_id
* @property int|null $status_id
@@ -69,6 +73,7 @@ use Monolog\Level;
* @method static Builder<static>|Entry whereDeletedAt($value)
* @method static Builder<static>|Entry whereDescription($value)
* @method static Builder<static>|Entry whereFeatured($value)
* @method static Builder<static>|Entry whereFeaturedAt($value)
* @method static Builder<static>|Entry whereGameId($value)
* @method static Builder<static>|Entry whereId($value)
* @method static Builder<static>|Entry whereLevelId($value)
@@ -90,12 +95,14 @@ use Monolog\Level;
* @method static Builder<static>|Entry whereYoutubeLink($value)
* @method static Builder<static>|Entry withTrashed(bool $withTrashed = true)
* @method static Builder<static>|Entry withoutTrashed()
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Spatie\Activitylog\Models\Activity> $activitiesAsSubject
* @property-read int|null $activities_as_subject_count
* @mixin \Eloquent
*/
class Entry extends Model
{
use SoftDeletes, HasGallery;
use SoftDeletes, HasGallery, LogsActivity;
/**
* @var string[]
@@ -108,6 +115,7 @@ class Entry extends Model
'main_image',
'state',
'featured',
'featured_at',
'game_id',
'platform_id',
'status_id',
@@ -121,7 +129,8 @@ class Entry extends Model
'comments_thread_id',
'staff_comment',
'rejected_at',
'level_id'
'level_id',
'created_at'
];
/**
@@ -131,6 +140,7 @@ class Entry extends Model
'featured' => 'boolean',
'release_date' => 'date',
'rejected_at' => 'datetime',
'featured_at' => 'datetime',
];
public function scopePublished( Builder $query ): Builder {
@@ -197,7 +207,7 @@ class Entry extends Model
return $this->hasMany(EntryHash::class);
}
public function parseStaffCredits(): array {
public function parseStaffCredits(): ?array {
return json_decode( $this->staff_credits ?? "", true );
}
@@ -205,10 +215,17 @@ class Entry extends Model
if( !$this->youtube_link )
return null;
$pattern = '%(?:https?://)?(?:www\.|m\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.com/(?:watch\?.*v=|embed/|v/|shorts/|live/))([\w-]{11})%i';
return EntryHelpers::getYoutubeVideoId( $this->youtube_link );
}
preg_match($pattern, $this->youtube_link, $matches);
return $matches[1] ?? null;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('entry')
->logAll()
->logOnlyDirty()
->dontLogEmptyChanges()
->setDescriptionForEvent(fn(string $eventName) => "Entry {$eventName}");
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @property int $id
@@ -36,6 +37,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereSecondaryOnlinePatcher($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereState($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereUpdatedAt($value)
* @property-read \App\Models\PlayOnlineSetting|null $playOnlineSetting
* @property int $download_count
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereDownloadCount($value)
* @mixin \Eloquent
*/
class EntryFile extends Model
@@ -53,4 +57,15 @@ class EntryFile extends Model
return $this->belongsTo(Entry::class);
}
public function playOnlineSetting(): HasOne
{
return $this->hasOne(PlayOnlineSetting::class,'file_id');
}
public function increaseDownloadCount(): void
{
$this->download_count++;
$this->save();
}
}

View File

@@ -4,5 +4,24 @@ namespace App\Models;
/**
* @deprecated Use Gallery instead.
* @property int $id
* @property string $galleryable_type
* @property int $galleryable_id
* @property string $image
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $galleryable
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereGalleryableId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereGalleryableType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereImage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereUpdatedAt($value)
* @property int $order
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereOrder($value)
* @mixin \Eloquent
*/
class EntryGallery extends Gallery {}

View File

@@ -19,6 +19,13 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereImage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereUpdatedAt($value)
* @property string $galleryable_type
* @property int $galleryable_id
* @property-read Model|\Eloquent $galleryable
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereGalleryableId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereGalleryableType($value)
* @property int $order
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereOrder($value)
* @mixin \Eloquent
*/
class Gallery extends Model

129
app/Models/LogXfUser.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Used only for logging details on XenForo user.
*
* Read-Only, must not be used in other things than logging.
*
* @property int $user_id
* @property string $username
* @property int $username_date
* @property int $username_date_visible
* @property string $email
* @property string $custom_title
* @property int $language_id
* @property int $style_id 0 = use system default
* @property string $style_variation
* @property string $timezone Example: 'Europe/London'
* @property int $visible Show browsing activity to others
* @property int $activity_visible
* @property int $user_group_id
* @property string $secondary_group_ids
* @property int $display_style_group_id User group ID that provides user styling
* @property int $permission_combination_id
* @property int $message_count
* @property int $question_solution_count
* @property int $conversations_unread
* @property int $register_date
* @property int $last_activity
* @property int|null $last_summary_email_date
* @property int $trophy_points
* @property int $alerts_unviewed
* @property int $alerts_unread
* @property int $avatar_date
* @property int $avatar_width
* @property int $avatar_height
* @property int $avatar_highdpi
* @property int $avatar_optimized
* @property string $gravatar If specified, this is an email address corresponding to the user's 'Gravatar'
* @property string $user_state
* @property string $security_lock
* @property int $is_moderator
* @property int $is_admin
* @property int $is_banned
* @property int $reaction_score
* @property int $vote_score
* @property int $warning_points
* @property int $is_staff
* @property string $secret_key
* @property int $privacy_policy_accepted
* @property int $terms_accepted
* @property int $rhpz_entry_count
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereActivityVisible($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAlertsUnread($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAlertsUnviewed($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarDate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarHeight($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarHighdpi($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarOptimized($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarWidth($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereConversationsUnread($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereCustomTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereDisplayStyleGroupId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereGravatar($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsAdmin($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsBanned($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsModerator($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsStaff($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLanguageId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLastActivity($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLastSummaryEmailDate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereMessageCount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser wherePermissionCombinationId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser wherePrivacyPolicyAccepted($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereQuestionSolutionCount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereReactionScore($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereRegisterDate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereRhpzEntryCount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecondaryGroupIds($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecretKey($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecurityLock($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereStyleId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereStyleVariation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTermsAccepted($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTimezone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTrophyPoints($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserGroupId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserState($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsername($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsernameDate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsernameDateVisible($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereVisible($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereVoteScore($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereWarningPoints($value)
* @mixin \Eloquent
*/
class LogXfUser extends Model
{
protected $connection = 'xenforo';
protected $table = 'user';
protected $primaryKey = 'user_id';
public $timestamps = false;
public $incrementing = true;
public function save(array $options = [])
{
return false;
}
public function delete()
{
return false;
}
public function update(array $attributes = [], array $options = [])
{
return false;
}
}

118
app/Models/News.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
namespace App\Models;
use App\Helpers\EntryHelpers;
use App\Traits\HasGallery;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $title
* @property string $slug
* @property int|null $category_id
* @property string $description
* @property string $state
* @property string|null $staff_comment
* @property \Illuminate\Support\Carbon|null $rejected_at
* @property int|null $entry_id
* @property string|null $relevant_link
* @property string|null $youtube_link
* @property int $user_id
* @property int|null $comments_thread_id
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Category|null $category
* @property-read \App\Models\Entry|null $entry
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Gallery> $gallery
* @property-read int|null $gallery_count
* @method static Builder<static>|News inQueue(int $daysRejected = 7)
* @method static Builder<static>|News newModelQuery()
* @method static Builder<static>|News newQuery()
* @method static Builder<static>|News onlyTrashed()
* @method static Builder<static>|News published()
* @method static Builder<static>|News query()
* @method static Builder<static>|News whereCategoryId($value)
* @method static Builder<static>|News whereCommentsThreadId($value)
* @method static Builder<static>|News whereCreatedAt($value)
* @method static Builder<static>|News whereDeletedAt($value)
* @method static Builder<static>|News whereDescription($value)
* @method static Builder<static>|News whereEntryId($value)
* @method static Builder<static>|News whereId($value)
* @method static Builder<static>|News whereRejectedAt($value)
* @method static Builder<static>|News whereRelevantLink($value)
* @method static Builder<static>|News whereSlug($value)
* @method static Builder<static>|News whereStaffComment($value)
* @method static Builder<static>|News whereState($value)
* @method static Builder<static>|News whereTitle($value)
* @method static Builder<static>|News whereUpdatedAt($value)
* @method static Builder<static>|News whereUserId($value)
* @method static Builder<static>|News whereYoutubeLink($value)
* @method static Builder<static>|News withTrashed(bool $withTrashed = true)
* @method static Builder<static>|News withoutTrashed()
* @mixin \Eloquent
*/
class News extends Model
{
use SoftDeletes, HasGallery;
protected $table = 'news';
protected $fillable = [
'title',
'slug',
'description',
'state',
'category_id',
'entry_id',
'relevant_link',
'youtube_link',
'user_id',
'comments_thread_id',
'staff_comment',
'rejected_at',
'created_at'
];
protected $casts = [
'rejected_at' => 'datetime',
];
public function scopePublished(Builder $query): Builder
{
return $query->where('state', 'published' );
}
public function scopeInQueue( Builder $query, int $daysRejected = 7 ): Builder
{
return $query->withTrashed()->where(function($q) use($daysRejected) {
$q->where('state', 'pending')->whereNull('deleted_at');
})->orWhere(function($q) use($daysRejected) {
$q->where('state', 'rejected')->whereNotNull('rejected_at')->where('rejected_at', '>=', now()->subDays($daysRejected) );
});
}
public function entry(): BelongsTo
{
return $this->belongsTo(Entry::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function getYoutubeVideoId(): ?string {
if( !$this->youtube_link )
return null;
return EntryHelpers::getYoutubeVideoId( $this->youtube_link );
}
}

View File

@@ -25,6 +25,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereShortName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereUpdatedAt($value)
* @property string|null $play_online_core
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform wherePlayOnlineCore($value)
* @mixin \Eloquent
*/
class Platform extends Model

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $file_id
* @property string $core
* @property bool $threads
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\EntryFile $file
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereCore($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereThreads($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereUpdatedAt($value)
* @mixin \Eloquent
*/
class PlayOnlineSetting extends Model
{
protected $primaryKey = 'file_id';
public $incrementing = false;
protected $keyType = 'int';
protected $fillable = [
'file_id',
'core',
'threads'
];
protected $casts = [
'threads' => 'boolean',
];
public function file(): BelongsTo
{
return $this->belongsTo(EntryFile::class, 'file_id');
}
}

165
app/Policies/NewsPolicy.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
namespace App\Policies;
use App\Models\Entry;
use App\Models\News;
use App\Auth\XenForoUser as User;
class NewsPolicy
{
public function viewAny(User $user): bool
{
if( $user->_can( 'romhackplaza', 'view' ) )
return true;
return false;
}
public function viewPending(User $user, News $news): bool
{
// Author.
if( $news->user_id === $user->user_id )
return true;
return $user->_can( 'romhackplaza', 'canModerateEntries' );
}
public function viewDraft(User $user, News $news): bool {
// Author.
if( $news->user_id === $user->user_id )
return true;
return $user->_can( 'romhackplaza', 'canSeeOthersDrafts' );
}
public function viewRejected(User $user, News $news): bool
{
// Author.
if( $news->user_id === $user->user_id )
return true;
return $user->_can( 'romhackplaza', 'canSeeRejectedEntries' );
}
public function viewHidden(User $user, News $news): bool
{
return $user->_can('romhackplaza', 'canSeeHiddenEntries' );
}
public function viewLocked(User $user, News $news): bool
{
// Author.
if( $news->user_id === $user->user_id )
return true;
return $user->_can('romhackplaza', 'canSeeLockedEntries' );
}
public function create(User $user, ?News $news = null ): bool
{
return $user->_can( 'romhackplaza', 'canSubmitEntry' );
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, News $news): bool
{
if( $news->state === 'published' ){
// Staff editors
if( $user->_can('romhackplaza', 'canEditOthersEntries') )
return true;
// Author.
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
return true;
return false;
} else if( $news->state === 'pending' ){
// Staff moderation.
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can('romhackplaza', 'canModerateEntries') )
return true;
// Author.
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
return true;
} else if( $news->state === 'draft' ){
// Staff.
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeOthersDrafts' ) )
return true;
// Author.
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
return true;
} else if( $news->state === 'rejected' ){
// Staff.
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeRejectedEntries' ) )
return true;
// Author.
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
return true;
} else if( $news->state === 'locked' ){
// Staff.
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeLockedEntries' ) )
return true;
return false;
} else if( $news->state === 'hidden' ){
// Staff.
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeHiddenEntries' ) )
return true;
return false;
}
return false;
}
public function skipQueue(User $user, ?News $news = null): bool
{
return $user->_can( 'romhackplaza', 'canSubmitEntryInPublished' );
}
public function updateComment(User $user, News $news): bool
{
return $user->_can('romhackplaza', 'canModerateEntries' );
}
public function manageButtonsInQueue(User $user, News $news): bool
{
if( $news->state === 'rejected' ){
return $this->viewRejected( $user, $news );
}
return $user->_can('romhackplaza', 'canModerateEntries' );
}
public function approve(User $user, News $news): bool
{
return $user->_can('romhackplaza', 'canModerateEntries' );
}
public function reject(User $user, News $news): bool
{
return $user->_can('romhackplaza', 'canModerateEntries' );
}
public function moderate(User $user, News $news): bool
{
return $user->_can('romhackplaza', 'canModerateEntries' );
}
}

View File

@@ -6,8 +6,10 @@ use App\Auth\XenForoGuard;
use App\Auth\XenForoUser;
use App\Policies\TempFilePolicy;
use App\Services\TemporaryFileService;
use App\Support\XenForoCauserResolver;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
use Spatie\Activitylog\Support\CauserResolver;
class AppServiceProvider extends ServiceProvider
{
@@ -16,7 +18,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
$this->app->bind(CauserResolver::class, XenForoCauserResolver::class );
}
/**

View File

@@ -0,0 +1,223 @@
<?php
namespace App\Services;
use App\Models\Entry;
use App\Models\News;
use App\View\Components\EntryCard;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ActivityService
{
private const CACHE_ENTRIES = 300; // seconds.
private const CACHE_NEWS = 300; // seconds.
private const CACHE_MESSAGES = 300; // seconds.
private const CACHE_THREADS = 300; // seconds.
private const CACHE_CLUBS = 300; // seconds.
private const ITEMS_PER_TYPE = 15;
public function getActivities( array $activities = [ 'entries', 'news', 'messages', 'threads', 'clubs' ] ): Collection
{
$c = collect();
if( in_array( 'entries', $activities ) ) {
$c = $c->merge($this->getEntries());
}
if( in_array( 'news', $activities ) ) {
$c = $c->merge($this->getNews());
}
if( in_array( 'messages', $activities ) ) {
$c = $c->merge($this->getMessages());
}
if( in_array( 'threads', $activities ) ) {
$c = $c->merge($this->getThreads());
}
if( in_array( 'clubs', $activities ) ) {
$c = $c->merge($this->getClubs());
}
return $c->sortByDesc('date')
->values()
->take(30)
->map(function(array $item){
$obj = (object) $item;
$obj->date = Carbon::createFromTimestamp($obj->date);
return $obj;
});
}
private function formatEntry( Entry $entry ): array
{
return [
'type' => 'entry',
'title' => $entry->complete_title ?? $entry->title,
'url' => route('entries.show', ['section' => $entry->type, 'entry' => $entry]),
'image' => $entry->main_image ? \Storage::url($entry->main_image) : null,
'date' => $entry->created_at->timestamp,
'author' => $entry->authors->pluck('name')->implode(', '),
'user_id' => $entry->user_id,
'badge' => EntryCard::ENTRY_TYPES_BADGE[$entry->type],
'badge_class' => $entry->type,
'excerpt' => $entry->description ? \Str::limit(strip_tags($entry->description), 80) : null,
'meta' => $entry->getRealPlatform()?->name
];
}
private function formatNews( News $news ): array
{
return [
'type' => 'news',
'title' => $news->title,
'url' => route('news.show', ['news' => $news]),
'image' => $news->gallery()->first() ? \Storage::url($news->gallery()->first()->image) : null,
'date' => $news->created_at->timestamp,
'author' => null,
'user_id' => $news->user_id,
'badge' => 'News',
'badge_class' => 'news',
'excerpt' => $news->description ? \Str::limit(strip_tags($news->description), 80) : null,
'meta' => $news->category?->name
];
}
private function formatMessage( object $message ): array
{
return [
'type' => 'message',
'title' => $message->title,
'url' => xfRoute('threads/.' ) . $message->thread_id . '/post-' . $message->post_id,
'image' => null,
'date' => $message->post_date,
'author' => null,
'user_id' => $message->user_id,
'badge' => 'Post',
'badge_class' => 'message',
'excerpt' => $message->message ? \Str::limit(strip_tags($message->message), 80) : null,
'meta' => null
];
}
private function formatThread( object $thread ): array
{
return [
'type' => 'thread',
'title' => $thread->title,
'url' => xfRoute('threads/.' ) . $thread->thread_id,
'image' => null,
'date' => $thread->post_date,
'author' => null,
'user_id' => $thread->user_id,
'badge' => 'Thread',
'badge_class' => 'thread',
'excerpt' => $thread->message ? \Str::limit(strip_tags($thread->message), 80) : null,
'meta' => null
];
}
private function formatClub( object $club ): array
{
return [
'type' => 'club',
'title' => $club->title,
'url' => xfRoute('forums/.' ) . $club->node_id,
'image' => null, // TODO: Remplacer par banner_date
'date' => $club->club_creation_date,
'author' => null,
'user_id' => $club->user_id,
'badge' => 'Club',
'badge_class' => 'club',
'excerpt' => $club->description ? \Str::limit(strip_tags($club->description), 80) : null,
'meta' => null
];
}
private function getEntries(): array
{
return Cache::remember('activity_entries', self::CACHE_ENTRIES, function() {
return Entry::published()
->with(['authors', 'game.platform'])
->latest('created_at')
->limit(self::ITEMS_PER_TYPE)
->get()
->map($this->formatEntry(...))
->toArray();
});
}
private function getNews(): array
{
return Cache::remember('activity_news', self::CACHE_NEWS, function() {
return News::published()
->with('gallery')
->latest('created_at')
->limit(self::ITEMS_PER_TYPE)
->get()
->map($this->formatNews(...))
->toArray();
});
}
private function getMessages(): array
{
return Cache::remember('activity_messages', self::CACHE_MESSAGES, function() {
return DB::connection('xenforo')
->table('post')
->join('user', 'post.user_id', '=', 'user.user_id')
->join('thread', 'post.thread_id', '=', 'thread.thread_id')
->where('post.message_state', 'visible')
->where('thread.first_post_id', '!=', 'post.post_id')
->orderByDesc('post.post_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'thread.title', 'thread.thread_id', 'post.post_id', 'post.post_date',
'post.user_id', 'post.message'
])
->get()
->map($this->formatMessage(...))
->toArray();
});
}
private function getThreads(): array
{
return Cache::remember('activity_threads', self::CACHE_THREADS, function() {
return DB::connection('xenforo')
->table('thread')
->join('user', 'thread.user_id', '=', 'user.user_id')
->join('post', 'thread.first_post_id', '=', 'post.post_id')
->where('thread.discussion_state', 'visible')
->where('thread.discussion_type', '!=', 'redirect' )
->orderByDesc('thread.post_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'thread.title', 'thread.thread_id', 'thread.post_date', 'thread.user_id',
'post.message'
])
->get()
->map($this->formatThread(...))
->toArray();
});
}
private function getClubs(): array
{
return Cache::remember('activity_clubs', self::CACHE_CLUBS, function() {
return DB::connection('xenforo')
->table('club')
->where('club_state', 'visible')
->orderByDesc('club_creation_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'club.title', 'club.description', 'club.node_id',
'club.club_creation_date', 'club.user_id'
])
->get()
->map($this->formatClub(...))
->toArray();
});
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Services;
use App\Models\EntryFile;
use App\Models\LogXfUser;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
@@ -110,7 +111,6 @@ class FileServersService {
'filename' => $filename,
'current_chunk' => $currentChunk,
'total_chunks' => $totalChunks,
// TODO : Must replace User ID
'zeus' => $this->generateZeusToken( \Auth::user()->user_id, $server['base_url'], "Uploadchunk" ),
]);
@@ -129,4 +129,43 @@ class FileServersService {
return $data;
}
public function deleteFile(
string $filePath,
string $fileName,
int $userId
): bool {
foreach( $this->servers as $serverKey => $server ){
$response = Http::withHeaders([])
->post( $server['delete_file'], [
'filepath' => $filePath,
'filename' => $fileName,
'zeus' => $this->generateZeusToken( $userId, $server['base_url'], "Deletefile" ),
]);
if (!$response->successful()) {
throw new \RuntimeException( $response->body() );
}
$data = $response->json();
if( isset( $data['status'] ) && $data['status'] === 'deleted' ){
continue;
} else {
return false;
}
}
activity('entry-file')
->causedBy(LogXfUser::find($userId))
->withProperties([
'filepath' => $filePath,
'filename' => $fileName,
])
->event('file_deletion')
->log('File deleted');
return true;
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace App\Services;
use App\Helpers\EntryHelpers;
use App\Jobs\CreateXenForoCommentsThread;
use App\Models\Entry;
use App\Models\News;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class NewsService
{
/**
* Request for store/edit.
* @var Request|null
*/
private ?Request $request = null;
/**
* Entry for edit.
* @var News|null
*/
private ?News $news = null;
public function storeNews(Request $request): News
{
$this->request = $request;
$user_id = \Auth::user()->user_id;
$news = DB::transaction(function () use ($user_id) {
// STEP 1 : Slug
$slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class );
$fields = [
'title' => $this->request->input('title'),
'slug' => $slug,
'category_id' => $this->request->input('category'),
'description' => $this->request->input('description'),
'state' => $this->request->input('submit-state'),
'entry_id' => $this->request->input('entry_id'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_link'),
'user_id' => $user_id,
];
$news = News::create( $fields );
// STEP 3 : Prepare Gallery images.
$this->Step3a_PrepareGalleryImages( $news );
return $news;
});
$this->Step3b_SaveGalleryImages( $news );
$this->Step4_CreateCommentsThread( $news );
return $news;
}
private function Step3a_PrepareGalleryImages(News $news): void
{
foreach( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ){
$news->gallery()->create([
'image' => $imagePath,
'order' => $i
]);
}
}
private function Step3b_SaveGalleryImages(News $news): void
{
foreach ( $news->gallery ?? [] as $galleryItem ) {
$newPath = 'news/gallery-images/' . $news->id . '/' . basename($galleryItem->image);
if( !Storage::disk('public')->move($galleryItem->image, $newPath) )
continue;
$galleryItem->update(['image' => $newPath]);
}
}
private function Step4_CreateCommentsThread( News $news ): void
{
if( $news->state !== 'published' )
return;
if( !$news->comments_thread_id )
CreateXenForoCommentsThread::dispatch( $news );
}
public function editNews(Request $request, News $news): News
{
$this->request = $request;
$this->news = $news;
if( \Auth::user()->can('moderate', $news) ){
$user_id = $this->request->input('owner_user_id');
} else {
$user_id = \Auth::user()->user_id;
}
$galleryPaths = [];
$news = DB::transaction(function () use ($user_id, &$galleryPaths) {
// STEP 1 : Refresh slug.
if( $this->request->input('title') !== $this->news->title ){
$this->news->slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class, $this->news->id );
}
$fields = [
'title' => $this->request->input('title'),
'slug' => $this->news->slug,
'category_id' => $this->request->input('category'),
'description' => $this->request->input('description'),
'state' => $this->request->input('submit-state'),
'entry_id' => $this->request->input('entry_id'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_link'),
'user_id' => $user_id,
];
if( \Auth::user()->can('moderate', $this->news) ){
$fields['staff_comment'] = $this->request->input('staff_comment');
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
}
$this->news->update( $fields );
$galleryPaths = $this->eStep3a_UpdateGalleryImages();
return $this->news;
});
$this->eStep3b_UpdateGalleryImages( $galleryPaths );
$this->step4_CreateCommentsThread( $news );
return $news;
}
private function eStep3a_UpdateGalleryImages(): array
{
$requestGallery = $this->request->input('gallery', [] ) ?? [];
$existingGalleryPaths = $this->news->gallery->pluck('image')->toArray();
$needDeletion = array_diff( $existingGalleryPaths, $requestGallery );
if( !empty( $needDeletion ) ){
$this->news->gallery()->whereIn('image', $needDeletion )->delete();
}
$needAddition = array_diff( $requestGallery, $existingGalleryPaths );
$images = [];
foreach( $needAddition as $imagePath ){
$images[] = $this->news->gallery()->create([
'image' => $imagePath,
]);
}
foreach( $requestGallery as $i => $imagePath ){
$this->news->gallery()->where('image', $imagePath )->update(['order' => $i]);
}
return [ 'addition' => $images, 'deletion' => $needDeletion ];
}
private function eStep3b_UpdateGalleryImages( array $pathsChanges ): void
{
foreach ( $pathsChanges['deletion'] as $deletePath ){
if( Storage::disk('public')->exists($deletePath) )
Storage::disk('public')->delete($deletePath);
}
foreach ( $pathsChanges['addition'] as $galleryItem ){
$newPath = 'news/gallery-images/' . $this->news->id . '/' . basename( $galleryItem->image );
if( !Storage::disk('public')->move( $galleryItem->image, $newPath ) ){
continue;
}
$galleryItem->update(['image' => $newPath]);
}
}
}

View File

@@ -4,9 +4,11 @@ namespace App\Services;
use App\Exceptions\SubmissionException;
use App\Helpers\EntryHelpers;
use App\Helpers\PlayOnlineHelpers;
use App\Helpers\XenForoHelpers;
use App\Http\Requests\StoreEntryRequest;
use App\Jobs\CreateXenForoCommentsThread;
use App\Jobs\DeleteFile;
use App\Models\Author;
use App\Models\Category;
use App\Models\Entry;
@@ -76,8 +78,12 @@ class SubmissionsService {
'error' => null,
'uuid' => $uuid,
'state' => $file->state,
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
'meta_online_patcher' => $file->online_patcher,
'meta_secondary_online_patcher' => $file->secondary_online_patcher,
'meta_play_online' => $file->playOnlineSetting()->exists() ? true : false,
'meta_play_online_core' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->core : '',
'meta_play_online_threads' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->threads : false,
];
$file = Cache::get("uploaded_file_{$uuid}");
@@ -92,8 +98,12 @@ class SubmissionsService {
'error' => null,
'uuid' => $uuid,
'state' => $file['state'],
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
'meta_online_patcher' => false,
'meta_secondary_online_patcher' => false,
'meta_play_online' => false,
'meta_play_online_core' => null,
'meta_play_online_threads' => false
];
return null;
@@ -298,15 +308,20 @@ class SubmissionsService {
foreach ( $uuidData ?? [] as $uuid ) {
$fileData = Cache::pull("uploaded_file_{$uuid}");
if( !$fileData )
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry." );
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all the new files and retry." );
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
if( !$onlinePatcher )
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension( $fileData['filename'] );
if( section_must_be( [ 'romhacks', 'translations' ], $entry->type ) ) {
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
if (!$onlinePatcher)
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension($fileData['filename']);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
} else {
$onlinePatcher = false;
$secondaryOnlinePatcher = false;
}
EntryFile::create([
$file = EntryFile::create([
'entry_id' => $entry->id,
'file_uuid' => $uuid,
'filename' => $fileData['filename'],
@@ -319,6 +334,26 @@ class SubmissionsService {
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $entry->type ) ) {
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
if (!$playOnline && $entry->getRealPlatform()?->play_online_core !== null) {
$playOnline = true;
$playOnlineCore = $entry->getRealPlatform()?->play_online_core;
}
if ($playOnline) {
$file->playOnlineSetting()->updateOrCreate(
['file_id' => $file->id],
[
'core' => $playOnlineCore,
'threads' => $playOnlineThreads,
]
);
}
}
}
}
@@ -452,9 +487,10 @@ class SubmissionsService {
private function Step12a_PrepareGalleryImages( Entry $entry ): void
{
foreach ( $this->request->input('gallery', [] ) ?? [] as $imagePath ) {
foreach ( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ) {
$entry->gallery()->create([
'image' => $imagePath,
'order' => $i
]);
}
}
@@ -556,6 +592,10 @@ class SubmissionsService {
if( \Auth::user()->can('moderate', $this->entry) ){
$fields['staff_comment'] = $this->request->input('staff_comment');
$fields['featured'] = $this->request->input('featured') ?? false;
if( $fields['featured'] == true && $this->entry->featured_at === null )
$fields['featured_at'] = now();
if( $fields['featured'] == false )
$fields['featured_at'] = null;
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
}
@@ -666,6 +706,10 @@ class SubmissionsService {
$needDeletion = array_diff( $existingUuids, $requestUuids );
if( !empty( $needDeletion ) ){
$userId = \Auth::user()->user_id;
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->get()->each( function ( $f ) use ( $userId ) {
DeleteFile::dispatch( $f->filepath, $f->filename, $userId);
});
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->delete();
}
@@ -680,15 +724,45 @@ class SubmissionsService {
foreach( $stateMap as $uuid => $state ){
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
if( section_must_be( ['romhacks', 'translations'], $this->entry->type ) ) {
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
} else {
$onlinePatcher = false;
$secondaryOnlinePatcher = false;
}
EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')
->update([
'state' => $state,
'online_patcher' => $onlinePatcher,
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
$entryFile = EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')->first();
if( !$entryFile )
continue;
$entryFile->update([
'state' => $state,
'online_patcher' => $onlinePatcher,
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $this->entry->type ) ) {
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
if ($playOnline) {
if ($playOnlineCore === null || !in_array($playOnlineCore, PlayOnlineHelpers::getCoreLists()))
$playOnlineCore = $this->entry->getRealPlatform()->play_online_core ? $this->entry->getRealPlatform()->play_online_core : 'nes';
$entryFile->playOnlineSetting()->updateOrCreate(
['file_id' => $entryFile->id],
[
'core' => $playOnlineCore,
'threads' => $playOnlineThreads,
]
);
} else {
$entryFile->playOnlineSetting()->delete();
}
}
}
}
@@ -860,6 +934,10 @@ class SubmissionsService {
]);
}
foreach ( $requestGallery as $i => $imagePath ){
$this->entry->gallery()->where('image', $imagePath )->update(['order' => $i]);
}
return [ 'addition' => $images, 'deletion' => $needDeletion ];
}

View File

@@ -92,7 +92,7 @@ class XenforoApiService {
return $response['success'] ?? false;
}
public function createCommentsThread( Entry $entry ): bool
public function createCommentsThread( Entry|News $entry ): bool
{
if( !$entry->comments_thread_id || $entry->comments_thread_id <= 0 ){
$data = [
@@ -134,6 +134,14 @@ class XenforoApiService {
return $response['success'] ?? false;
}
public function featuredRequest( Entry $entry ): bool
{
$response = $this->post("romhackplaza_entry/featured", data: [
'entry_id' => $entry->id, 'user_id' => $entry->user_id, 'entry_title' => $entry->complete_title ?? $entry->title,
]);
return $response['success'] ?? false;
}
public function deleteThreadWithEntry(int $threadId): bool
{
return (bool) $this->delete( "threads/{$threadId}", data: ['reason' => "Deletion with entry." ] );

View File

@@ -195,9 +195,9 @@ class XenforoService {
}
private function hashCSRFToken( string $token ): string
private function hashCSRFToken( string $token, int $timestamp ): string
{
return hash_hmac('md5', $token . time(), config('app.xf_salt') );
return hash_hmac('md5', $token . $timestamp, config('app.xf_salt') );
}
public function getCSRFToken(): string
{
@@ -207,6 +207,28 @@ class XenforoService {
Cookie::queue('xf_csrf', $token, 0, '/', config('session.domain'), 0, false, false );
}
return time() . ',' . $this->hashCSRFToken($token);
$timestamp = time();
return $timestamp . ',' . $this->hashCSRFToken($token, $timestamp);
}
public function verifyCSRFToken( string $requestToken ): bool
{
$token = Cookie::get('xf_csrf');
if( !$token ){
return false;
}
try {
[$timestamp, $hash] = explode(',', $requestToken);
} catch (\Throwable $th) {
return false;
}
$timestamp = intval($timestamp);
$currentTimestamp = time();
if( abs( $currentTimestamp - $timestamp ) > 3600 )
return false;
return $hash === $this->hashCSRFToken($token, $timestamp);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Support;
use App\Auth\XenForoUser;
use App\Models\LogXfUser;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Support\CauserResolver;
class XenForoCauserResolver extends CauserResolver
{
public function getDefaultCauser(): ?Model
{
$user = \Auth::user();
if( $user instanceof XenForoUser && $user->getAuthIdentifier() ){
return LogXfUser::find($user->getAuthIdentifier());
}
return null;
}
}

View File

@@ -9,6 +9,6 @@ trait HasGallery
{
public function gallery(): MorphMany
{
return $this->morphMany(Gallery::class, 'galleryable')->orderBy('id');
return $this->morphMany(Gallery::class, 'galleryable')->orderBy('order')->orderBy('id');
}
}

View File

@@ -19,7 +19,8 @@ class CategorySelector extends Component
public function __construct(
public string $section,
public array $selected = [],
public bool $required = true
public bool $required = true,
public bool $news = false
)
{
$this->categories = Category::query()
@@ -36,6 +37,6 @@ class CategorySelector extends Component
*/
public function render(): View|Closure|string
{
return view('components.category-selector');
return $this->news === true ? view('components.news-category-selector' ) : view('components.category-selector');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\View\Components;
use App\Models\Entry;
use App\Models\News;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class NewsCard extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public News $news
)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.news-card');
}
}

View File

@@ -3,6 +3,7 @@
namespace App\View\Components;
use App\Models\Entry;
use App\Models\News;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
@@ -16,11 +17,16 @@ class SubmitEntryStatus extends Component
public string $section,
public bool $isEdit,
public ?string $currentState,
public null|string|Entry $entry = 'App\Models\Entry',
public null|string|Entry|News $entry = null,
public bool $news = false
)
{
if( $this->entry === null )
$this->entry = 'App\Models\Entry';
if( $this->entry === null ){
if( $news )
$this->entry = 'App\Models\News';
else
$this->entry = 'App\Models\Entry';
}
}
public function availableStates(): array

View File

@@ -25,6 +25,15 @@ if( !function_exists( 'section_must_not_be' ) ){
}
if( !function_exists('userTheme' ) ){
function userTheme(): string {
if( !\Auth::guest() ){
return \Auth::user()->style_variation ?? 'default';
}
return \Illuminate\Support\Facades\Cookie::get('xf_style_variation', 'default');
}
}
if( !function_exists( 'databaseRoute' ) ){
function databaseRoute( array $params = [] ): string