Initial commit

This commit is contained in:
2026-05-20 18:25:15 +02:00
commit 95f0b4ff01
288 changed files with 90909 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Services;
use App\Models\EntryFile;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class FileServersService {
public const FAVORITE_SERVER_TIME = 3600; // In seconds.
private const ZEUS_TOKEN_EXPIRATION = 600; // In seconds.
private string $apiKey;
private array $servers;
public function __construct()
{
$this->apiKey = config('fileservers.secret_key');
$this->servers = config('fileservers.servers');
}
public function generateZeusToken( int $user_id, string $to, string $action ){
$info = [
'user_id' => $user_id,
'to' => $to,
'action' => $action,
'generated_at' => time(),
'expires_at' => time() + self::ZEUS_TOKEN_EXPIRATION,
'romhackplaza' => bin2hex(random_bytes(16)),
];
$json = json_encode( $info );
$sig = hash_hmac( 'sha256', $json, $this->apiKey );
$end = base64_encode( $json ) . "|" . $sig;
return $end;
}
public function getRandomServerKey(): string
{
$keys = array_keys($this->servers);
return $keys[array_rand($keys)];
}
public function getEntryFileServerKey( EntryFile $file ): string
{
$serverKey = null;
if( $file->favorite_server !== null && $file->favorite_at !== null ) {
if( time() < $file->favorite_at + static::FAVORITE_SERVER_TIME ) {
$serverKey = $file->favorite_server;
}
}
if( $serverKey === null || !isset( $this->servers[$serverKey] ) ) {
$serverKey = $this->getRandomServerKey();
}
return $serverKey;
}
/**
* Download URL requires 'filepath' and 'filename'.
*
* @param EntryFile $file
* @return string
*/
public function getDownloadFileUrl( EntryFile $file ): string
{
$serverKey = $this->getEntryFileServerKey( $file );
$url = $this->servers[$serverKey]['download'] ?? "#";
if( $url === "#" )
return $url;
return $url . "&" . http_build_query( [ 'filename' => $file->filename, 'filepath' => $file->filepath ] );
}
/**
* @throws ConnectionException
*/
public function uploadChunk(
UploadedFile $chunk,
string $fileUUID,
int $currentChunk,
int $totalChunks,
string $filename,
string $type
){
// Define or get favorite server.
if( $currentChunk === 0 ){
$serverKey = $this->getRandomServerKey();
Cache::put("favorite_server_{$fileUUID}", $serverKey, now()->addHours(2) );
} else {
$serverKey = Cache::get( "favorite_server_{$fileUUID}" );
abort_if( !$serverKey, 422, "File upload expired, please retry." );
}
$server = $this->servers[$serverKey];
$filepath = $type . '/' . $fileUUID;
$response = Http::withHeaders([])
->attach( 'file', file_get_contents( $chunk->getRealPath() ), $fileUUID )
->post( $server['upload_chunk'], [
'filepath' => $filepath,
'filename' => $filename,
'current_chunk' => $currentChunk,
'total_chunks' => $totalChunks,
// TODO : Must replace User ID
'zeus' => $this->generateZeusToken( 0, $server['base_url'], "Uploadchunk" ),
]);
if (!$response->successful()) {
throw new \RuntimeException( $response->body() );
}
$data = $response->json();
if( isset( $data['file'] ) && $data['file'] !== false ){
Cache::forget( "favorite_server_{$fileUUID}" );
}
$data['favorite_server'] = $serverKey;
$data['file_uuid'] = $fileUUID;
$data['file_path'] = $filepath;
return $data;
}
}

View File

@@ -0,0 +1,388 @@
<?php
namespace App\Services;
use App\Exceptions\SubmissionException;
use App\Helpers\EntryHelpers;
use App\Http\Requests\StoreEntryRequest;
use App\Models\Author;
use App\Models\Entry;
use App\Models\EntryFile;
use App\Models\EntryGallery;
use App\Models\EntryHash;
use App\Models\Game;
use App\Models\Genre;
use App\Models\Language;
use App\Models\Modification;
use App\Models\Platform;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
/**
* @phpstan-import-type FSFileData from \App\Types\FSTypes
*/
class SubmissionsService {
/**
* Request for store/edit.
* @var StoreEntryRequest|null
*/
private ?StoreEntryRequest $request = null;
/**
* Section for store/edit.
* @var string|null
*/
private ?string $section = null;
/**
* @return list<FSFileData>
*/
public function prepareOldFiles( ?Entry $entry = null ): array
{
if( $entry === null ){
$files = old( 'files_uuid', [] );
} else {
$files = old( 'files_uuid', $entry->files->pluck('file_uuid')->toArray() );
}
if( $files === [] )
return [];
return array_map(
function( string $uuid ) {
$file = EntryFile::where('file_uuid', $uuid)->first();
if( $file )
return [
'name' => $file->filename,
'totalChunks' => 0, // Already uploaded.
'rawFile' => null,
'progressValue' => 0,
'currentChunk' => 0,
'done' => true,
'error' => null,
'uuid' => $uuid
];
$file = Cache::get("uploaded_file_{$uuid}");
if( $file )
return [
'name' => $file['filename'],
'totalChunks' => 0, // Already uploaded.
'rawFile' => null,
'progressValue' => 0,
'currentChunk' => 0,
'done' => true,
'error' => null,
'uuid' => $uuid
];
return null;
},
$files );
}
/**
* @param StoreEntryRequest $request
* @param string $section
*
* @return Entry
* @throws SubmissionException
* @throws \Throwable
*/
public function storeEntry( StoreEntryRequest $request, string $section ){
// STEP 1 : Prepare basic fields.
$this->request = $request;
$this->section = $section;
$user_id = 0; // TODO: Replace that.
$entry = DB::transaction(function () use ( $user_id ) {
// STEP 2 : Create game.
$gameId = null;
if( section_must_be( ['romhacks', 'translations'], $this->section ) ){
$gameId = $this->Step2_CreateAndReturnGameId();
}
// STEP 3 : Create Complete title.
$completeTitle = $this->Step3_BuildCompleteTitle( $gameId );
// STEP 4 : Generate slug and entry title.
$entrySlug = EntryHelpers::uniqueSlug( $completeTitle, Entry::class );
if( section_must_be( 'translations', $this->section ) &&
!$this->request->input('entry_title') ){
$entryTitle = Game::find($gameId)->name;
} else {
$entryTitle = $this->request->input('entry_title');
}
// STEP 5 : Removed / Delayed.
// $mainImage = $this->Step5_MoveMainImage();
// STEP 6 : Prepare entry fields and save entry.
$fields = [
'type' => $this->section,
'title' => $entryTitle,
'slug' => $entrySlug,
'description' => $this->request->input('description'),
'main_image' => $this->request->input('main-image'),
'state' => $this->request->input('submit-state'),
'game_id' => $gameId,
'status_id' => $this->request->input('status'),
'version' => $this->request->input('version'),
'release_date' => $this->request->input('release-date'),
'staff_credits' => $this->request->input('staff_credits'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_video'),
'user_id' => $user_id,
'complete_title' => $completeTitle,
];
$entry = Entry::create( $fields );
// STEP 7 : Save entry fields.
$this->Step7_SaveEntryFiles( $entry->id );
// STEP 8 : Save hashes.
$this->Step8_SaveHashes( $entry->id );
// STEP 9 : Save Authors.
$this->Step9_SaveAuthors( $entry );
// STEP 10 : Save Modifications.
if( section_must_be( 'romhacks', $this->section ) ){
$this->Step10_SaveRomhacksModifications( $entry );
}
// STEP 11 : Save Languages
$this->Step11_SaveLanguages( $entry );
// STEP 12 : Prepare Gallery images.
$this->Step12a_PrepareGalleryImages( $entry );
return $entry;
});
// Step 12, Move main image and gallery.
$this->Step12b_MoveMainImage( $entry );
$this->Step12c_SaveGalleryImages( $entry );
return $entry;
}
/**
* @return int
*
* @throws SubmissionException
*/
private function Step2_CreateAndReturnGameId(): int {
// Already existing game.
if( $this->request->input('game_id') )
return $this->request->input('game_id');
// Need to create a game.
if( !$this->request->input('new-game-title') || !$this->request->input('new-game-platform') || !$this->request->input('new-game-genre') )
throw new SubmissionException( "New game informations is missing" );
$platform = Platform::find( $this->request->input('new-game-platform') );
$genre = Genre::find( $this->request->input('new-game-genre') );
if( !$platform || !$genre )
throw new SubmissionException( "Incorrect game platform id" );
$gameSlug = EntryHelpers::uniqueSlug( $this->request->input('new-game-title'), Game::class );
$game = Game::create([
'name' => trim( $this->request->input('new-game-title') ),
'slug' => $gameSlug,
'platform_id' => $platform->id,
'genre_id' => $genre->id,
]);
return $game->id;
}
/**
* Prepare and build complete title.
*
* @param int|null $gameId
*
* @return string
*/
private function Step3_BuildCompleteTitle( ?int $gameId = null ): string {
$fields = [];
$fields['entry_title'] = $this->request->input('entry_title') ?? null;
if( section_must_be( [ 'homebrew', 'translations' ], $this->section ) && $gameId ){
$fields['game_name'] = Game::find( $gameId )->name;
}
if( section_must_be( 'translations', $this->section ) ) {
$fields['languages_string'] = Language::whereIn('id', $this->request->input('languages', []))->pluck('name')->implode(', ');
}
if( section_must_be(['romhacks', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) {
// TODO: Add single platform ID compatibility.
$fields['platform_name'] = Game::find( $gameId )->platform->name;
}
return EntryHelpers::buildCompleteTitle( $this->section, $fields );
}
/**
* @param int $entryId
*
* @return void
* @throws SubmissionException
*/
private function Step7_SaveEntryFiles( int $entryId ): void
{
foreach ( $this->request->input('files_uuid', [] ) as $uuid ) {
$fileData = Cache::pull("uploaded_file_{$uuid}");
if( !$fileData )
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry." );
EntryFile::create([
'entry_id' => $entryId,
'file_uuid' => $uuid,
'filename' => $fileData['filename'],
'filepath' => $fileData['filepath'],
'favorite_server' => $fileData['favorite_server'],
'favorite_at' => \DateTimeImmutable::createFromTimestamp( $fileData['favorite_at'] ),
'filesize' => $fileData['filesize'],
'state' => 'public'
]);
}
}
/**
* @param int $entryId
*
* @return void
*/
private function Step8_SaveHashes( int $entryId ): void
{
foreach ( $this->request->input('hashes', [] ) as $hash ) {
if( !isset($hash['filename'], $hash['hash_crc32'], $hash['hash_sha1'], $hash['verified']) ) {
continue;
}
EntryHash::create([
'entry_id' => $entryId,
'filename' => $hash['filename'],
'hash_crc32' => $hash['hash_crc32'],
'hash_sha1' => $hash['hash_sha1'],
'verified' => $hash['verified'],
]);
}
}
/**
* @param Entry $entry
*
* @return void
* @throws SubmissionException
*/
private function Step9_SaveAuthors( Entry $entry ): void
{
// Existing authors.
foreach ( $this->request->input('authors', [] ) as $authorId ) {
$author = Author::find( $authorId );
if( !$author )
throw new SubmissionException( "Author {$authorId} does not exist." );
$entry->authors()->attach( $author->id );
}
// New Authors
foreach ( $this->request->input('new-authors', [] ) as $authorName ) {
$authorName = trim( $authorName );
if( $authorName === '' )
continue;
$author = Author::firstOrCreate(
['slug' => EntryHelpers::uniqueSlug( $authorName, Author::class )],
['name' => $authorName]
);
$entry->authors()->attach( $author->id );
}
}
/**
* @param Entry $entry
*
* @return void
* @throws SubmissionException
*/
private function Step10_SaveRomhacksModifications( Entry $entry ): void
{
foreach ( $this->request->input('modifications', [] ) as $modificationId ) {
$modification = Modification::find( $modificationId );
if( !$modification )
throw new SubmissionException( "Modification {$modificationId} does not exist." );
$entry->modifications()->attach( $modification->id );
}
}
/**
* @param Entry $entry
*
* @return void
* @throws SubmissionException
*/
private function Step11_SaveLanguages( Entry $entry ): void
{
foreach ( $this->request->input('languages', [] ) as $languageId ) {
$language = Language::find( $languageId );
if( !$language )
throw new SubmissionException( "Language {$languageId} does not exist." );
$entry->languages()->attach( $language->id );
}
}
private function Step12a_PrepareGalleryImages( Entry $entry ): void
{
foreach ( $this->request->input('gallery', [] ) as $imagePath ) {
EntryGallery::create([
'entry_id' => $entry->id,
'image' => $imagePath,
]);
}
}
/**
* @param Entry $entry
*
* @return void
*/
private function Step12b_MoveMainImage( Entry $entry ): void {
$mainImage = $entry->main_image;
$newPath = 'entries/main-images/' . basename($mainImage);
if( !Storage::disk('public')->move($mainImage, $newPath) )
return;
$entry->update(['main_image' => $newPath]);
}
private function Step12c_SaveGalleryImages( Entry $entry ): void
{
foreach ( $entry->gallery as $galleryItem ) {
$newPath = 'entries/gallery-images/' . $entry->id . '/' . basename($galleryItem->image);
if( !Storage::disk('public')->move($galleryItem->image, $newPath) )
continue;
$galleryItem->update(['image' => $newPath]);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
class TemporaryFileService {
public const int NB_HOURS_FILES_KEPT = 6;
/**
* Upload a file in the temporary path.
*
* @param UploadedFile|null $file
*
* @return string|bool
*/
public function uploadFile(?UploadedFile $file ): string|bool {
if( !$file )
return false;
return $file->store( 'temp', 'public' );
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class XenforoService {
private const array PERMISSIONS_KEPT = [ 'general', 'romhackplaza' ];
private const int TTL_PERMISSIONS = 300;
public function getPermissions(int $userId, int $permissionCombinationId): array {
return Cache::remember("xf_permissions_{$userId}", self::TTL_PERMISSIONS, function() use($permissionCombinationId) {
$row = \DB::connection('xenforo')
->table('permission_combination')
->where('permission_combination_id', $permissionCombinationId)
->value('cache_value');
if( !$row )
return [];
$data = json_decode($row, true);
$data = array_intersect_key($data, array_flip(self::PERMISSIONS_KEPT));
return $data ?: [];
});
}
public function clearUserData(int $userId): void
{
Cache::forget("xf_permissions_{$userId}");
}
}