Improve and optimize code

This commit is contained in:
DariusIII
2025-11-06 11:40:42 +01:00
parent d45e5d739d
commit 1882275fa4
+313 -105
View File
@@ -8,169 +8,377 @@ use App\Models\UserDownload;
use App\Models\UsersRelease;
use Blacklight\NZB;
use Blacklight\utility\Utility;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipStream\ZipStream;
class GetNzbController extends BasePageController
{
/**
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\STS\ZipStream\ZipStream|\Symfony\Component\HttpFoundation\StreamedResponse
*
* @throws \Exception
*/
public function getNzb(Request $request, $guid = null)
{
private const int BUFFER_SIZE = 1000000;
// If guid is passed as URL parameter, merge it into request as 'id'
private const string NZB_SUFFIX = '.nzb';
/**
* Download NZB file(s) for authenticated users
*
* @return Application|ResponseFactory|\Illuminate\Foundation\Application|JsonResponse|Response|ZipStream|StreamedResponse
*
* @throws Exception
*/
public function getNzb(Request $request, ?string $guid = null)
{
// Normalize guid parameter
$this->normalizeGuidParameter($request, $guid);
// Authenticate and authorize user
$userData = $this->authenticateUser($request);
if (! \is_array($userData)) {
return $userData; // Return error response
}
['uid' => $uid, 'userName' => $userName, 'maxDownloads' => $maxDownloads, 'rssToken' => $rssToken] = $userData;
// Check download limits
$downloadLimitError = $this->checkDownloadLimit($uid, $maxDownloads);
if ($downloadLimitError !== null) {
return $downloadLimitError;
}
// Validate and sanitize ID parameter
$releaseId = $this->validateAndSanitizeId($request);
if (! \is_string($releaseId)) {
return $releaseId; // Return error response
}
// Handle zip download request
if ($this->isZipRequest($request)) {
return $this->handleZipDownload($request, $uid, $userName, $maxDownloads, $releaseId);
}
// Handle single NZB download
return $this->handleSingleNzbDownload($request, $uid, $rssToken, $releaseId);
}
/**
* Normalize the guid parameter into the request
*/
private function normalizeGuidParameter(Request $request, ?string $guid): void
{
if ($guid !== null && ! $request->has('id')) {
$request->merge(['id' => $guid]);
}
}
// Page is accessible only by the rss token, or logged in users.
/**
* Authenticate user via session or RSS token
*
* @return array<string, mixed>|Response
*/
private function authenticateUser(Request $request)
{
// Try session authentication first
if ($request->user()) {
$uid = $this->userdata->id;
$userName = $this->userdata->username;
$maxDownloads = $this->userdata->role->downloadrequests;
$rssToken = $this->userdata->api_token;
if ($this->userdata->hasRole('Disabled')) {
return Utility::showApiError(101);
}
} else {
if ($request->missing('r')) {
return Utility::showApiError(200);
}
$res = User::getByRssToken($request->input('r'));
if (! $res) {
return Utility::showApiError(100);
}
$uid = $res['id'];
$userName = $res->username;
$rssToken = $res['api_token'];
$maxDownloads = $res->role->downloadrequests;
if ($res->hasRole('Disabled')) {
return Utility::showApiError(101);
}
return $this->getUserDataFromSession();
}
// Check download limit on user role.
// Try RSS token authentication
return $this->getUserDataFromRssToken($request);
}
/**
* Get user data from authenticated session
*
* @return array<string, mixed>|Response
*/
private function getUserDataFromSession()
{
if ($this->userdata->hasRole('Disabled')) {
return Utility::showApiError(101);
}
return [
'uid' => $this->userdata->id,
'userName' => $this->userdata->username,
'maxDownloads' => $this->userdata->role->downloadrequests,
'rssToken' => $this->userdata->api_token,
];
}
/**
* Get user data from RSS token
*
* @return array<string, mixed>|Response
*/
private function getUserDataFromRssToken(Request $request)
{
if ($request->missing('r')) {
return Utility::showApiError(200);
}
$user = User::getByRssToken($request->input('r'));
if (! $user) {
return Utility::showApiError(100);
}
if ($user->hasRole('Disabled')) {
return Utility::showApiError(101);
}
return [
'uid' => $user->id,
'userName' => $user->username,
'maxDownloads' => $user->role->downloadrequests,
'rssToken' => $user->api_token,
];
}
/**
* Check if user has exceeded download limits
*
* @return Response|null
*
* @throws Exception
*/
private function checkDownloadLimit(int $uid, int $maxDownloads): mixed
{
$requests = UserDownload::getDownloadRequests($uid);
if ($requests > $maxDownloads) {
return Utility::showApiError(501);
}
if (! $request->input('id')) {
return null;
}
/**
* Validate and sanitize the release ID parameter
*
* @return string|Response
*/
private function validateAndSanitizeId(Request $request)
{
$id = $request->input('id');
if (empty($id)) {
return Utility::showApiError(200, 'Parameter id is required');
}
// Remove any suffixed id with .nzb which is added to help weblogging programs see nzb traffic.
$request->merge(['id' => str_ireplace('.nzb', '', $request->input('id'))]);
// Remove .nzb suffix if present
$sanitizedId = str_ireplace(self::NZB_SUFFIX, '', $id);
$request->merge(['id' => $sanitizedId]);
// User requested a zip of guid,guid,guid releases.
if ($request->has('zip') && $request->input('zip') === '1') {
$guids = explode(',', $request->input('id'));
if ($requests + \count($guids) > $maxDownloads) {
return Utility::showApiError(501);
}
return $sanitizedId;
}
$zip = getStreamingZip($guids);
if ($zip !== '') {
User::incrementGrabs($uid, \count($guids));
foreach ($guids as $guid) {
Release::updateGrab($guid);
UserDownload::addDownloadRequest($uid, $guid);
/**
* Check if this is a zip download request
*/
private function isZipRequest(Request $request): bool
{
return $request->has('zip') && $request->input('zip') === '1';
}
if ($request->has('del') && (int) $request->input('del') === 1) {
UsersRelease::delCartByUserAndRelease($guid, $uid);
}
}
/**
* Handle zip download of multiple releases
*
* @return JsonResponse|ZipStream|StreamedResponse
*
* @throws Exception
*/
private function handleZipDownload(
Request $request,
int $uid,
string $userName,
int $maxDownloads,
string $releaseId
) {
$guids = explode(',', $releaseId);
$guidCount = \count($guids);
Log::channel('zipped')->info('User '.$userName.' downloaded zipped files from site with IP: '.$request->ip());
return $zip;
}
// Check if zip download would exceed limits
$requests = UserDownload::getDownloadRequests($uid);
if ($requests + $guidCount > $maxDownloads) {
return Utility::showApiError(501);
}
$zip = getStreamingZip($guids);
if ($zip === '') {
return response()->json(['message' => 'Unable to create .zip file'], 404);
}
$nzbPath = (new NZB)->getNZBPath($request->input('id'));
// Update statistics
$this->updateZipDownloadStatistics($request, $uid, $guids);
Log::channel('zipped')->info("User {$userName} downloaded zipped files from site with IP: {$request->ip()}");
return $zip;
}
/**
* Update statistics for zip downloads
*/
private function updateZipDownloadStatistics(Request $request, int $uid, array $guids): void
{
$guidCount = \count($guids);
User::incrementGrabs($uid, $guidCount);
$shouldDeleteFromCart = $request->has('del') && (int) $request->input('del') === 1;
foreach ($guids as $guid) {
Release::updateGrab($guid);
UserDownload::addDownloadRequest($uid, $guid);
if ($shouldDeleteFromCart) {
UsersRelease::delCartByUserAndRelease($guid, $uid);
}
}
}
/**
* Handle single NZB file download
*
* @return Response|StreamedResponse
*/
private function handleSingleNzbDownload(
Request $request,
int $uid,
string $rssToken,
string $releaseId
) {
// Get NZB file path and validate
$nzbPath = (new NZB)->getNZBPath($releaseId);
if (! File::exists($nzbPath)) {
return Utility::showApiError(300, 'NZB file not found!');
}
$relData = Release::getByGuid($request->input('id'));
if ($relData === null) {
// Get release data
$releaseData = Release::getByGuid($releaseId);
if ($releaseData === null) {
return Utility::showApiError(300, 'Release not found!');
}
// Update release and user actions
Release::updateGrab($request->input('id'));
UserDownload::addDownloadRequest($uid, $relData['id']);
// Update statistics
$this->updateDownloadStatistics($request, $uid, $releaseId, $releaseData['id']);
// Build response headers
$headers = $this->buildNzbHeaders($releaseId, $uid, $rssToken, $releaseData);
// Stream modified NZB content
$cleanName = $this->sanitizeFilename($releaseData['searchname']);
return response()->streamDownload(
fn () => $this->streamModifiedNzbContent($nzbPath, $uid),
$cleanName.self::NZB_SUFFIX,
$headers
);
}
/**
* Update download statistics for single NZB
*/
private function updateDownloadStatistics(Request $request, int $uid, string $releaseId, int $releaseDbId): void
{
Release::updateGrab($releaseId);
UserDownload::addDownloadRequest($uid, $releaseDbId);
User::incrementGrabs($uid);
if ($request->has('del') && (int) $request->input('del') === 1) {
UsersRelease::delCartByUserAndRelease($request->input('id'), $uid);
UsersRelease::delCartByUserAndRelease($releaseId, $uid);
}
}
// Build headers
/**
* Build headers for NZB download response
*
* @param array<string, mixed> $releaseData
* @return array<string, string>
*/
private function buildNzbHeaders(string $releaseId, int $uid, string $rssToken, array $releaseData): array
{
$headers = [
'Content-Type' => 'application/x-nzb',
'Expires' => now()->addYear()->toRfc7231String(),
'X-DNZB-Failure' => url('/failed').'?guid='.$request->input('id').'&userid='.$uid.'&api_token='.$rssToken,
'X-DNZB-Category' => e($relData['category_name']), // Escape category name
'X-DNZB-Details' => url('/details/'.$request->input('id')),
'X-DNZB-Failure' => url('/failed')."?guid={$releaseId}&userid={$uid}&api_token={$rssToken}",
'X-DNZB-Category' => e($releaseData['category_name']),
'X-DNZB-Details' => url("/details/{$releaseId}"),
];
if (! empty($relData['imdbid']) && $relData['imdbid'] > 0) {
$headers['X-DNZB-MoreInfo'] = 'http://www.imdb.com/title/tt'.$relData['imdbid'];
} elseif (! empty($relData['tvdb']) && $relData['tvdb'] > 0) {
$headers['X-DNZB-MoreInfo'] = 'http://www.thetvdb.com/?tab=series&id='.$relData['tvdb'];
// Add optional metadata headers
if (! empty($releaseData['imdbid']) && $releaseData['imdbid'] > 0) {
$headers['X-DNZB-MoreInfo'] = "http://www.imdb.com/title/tt{$releaseData['imdbid']}";
} elseif (! empty($releaseData['tvdb']) && $releaseData['tvdb'] > 0) {
$headers['X-DNZB-MoreInfo'] = "http://www.thetvdb.com/?tab=series&id={$releaseData['tvdb']}";
}
if ((int) $relData['nfostatus'] === 1) {
$headers['X-DNZB-NFO'] = url('/nfo/'.$request->input('id'));
if ((int) $releaseData['nfostatus'] === 1) {
$headers['X-DNZB-NFO'] = url("/nfo/{$releaseId}");
}
$headers += ['X-DNZB-RCode' => '200',
'X-DNZB-RText' => 'OK, NZB content follows.', ];
$headers['X-DNZB-RCode'] = '200';
$headers['X-DNZB-RText'] = 'OK, NZB content follows.';
// Raising this value may increase performance
$buffer_size = 1000000;
$bytes = '696';
return $headers;
}
// Open our file (in binary mode)
$zd = gzopen($nzbPath, 'rb');
$insert = ' <file poster="_ZWsSmvpc3Cl@HmhGEn.16m'.$uid.'" date="'.now()->timestamp.'" subject="&quot;release.nfo&quot; yEnc (1/1) 546">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="'.$bytes.'" number="1">o9AgxKI0_40wnnFGymLkxrDxt@EggKOp4.48R</segment>
</segments>
</file>';
$final = '';
// Keep repeating until the end of the input file
while (! gzeof($zd)) {
// Read buffer-size bytes
$contents = gzread($zd, $buffer_size);
$tmp = preg_replace('/file poster=\"/i', 'file poster="'.$uid.'-', $contents, 10);
$final .= $tmp;
/**
* Stream modified NZB content with user-specific modifications
*/
private function streamModifiedNzbContent(string $nzbPath, int $uid): void
{
$fileHandle = gzopen($nzbPath, 'rb');
if ($fileHandle === false) {
return;
}
$final = preg_replace('/<\/nzb>/i', $insert.PHP_EOL.'</nzb>'.PHP_EOL, $final, 1);
$buffer = '';
$lastChunk = false;
gzclose($zd);
// Stream and modify content in chunks
while (! gzeof($fileHandle)) {
$chunk = gzread($fileHandle, self::BUFFER_SIZE);
if ($chunk === false) {
break;
}
// Sanitize file name
$cleanName = str_replace([',', ' ', '/', '\\'], '_', $relData['searchname']);
// Combine with previous buffer to handle boundaries
$buffer .= $chunk;
return response()->streamDownload(function () use ($final) {
echo $final;
}, $cleanName.'.nzb', $headers);
// Check if this is the last chunk
if (gzeof($fileHandle)) {
$lastChunk = true;
}
// Process buffer
if ($lastChunk) {
// On last chunk, modify poster attributes
$buffer = preg_replace('/file poster="/', 'file poster="'.$uid.'-', $buffer);
echo $buffer;
} else {
// For intermediate chunks, keep some data in buffer to handle boundaries
$safeLength = mb_strlen($buffer) - 1000; // Keep last 1KB in buffer
if ($safeLength > 0) {
$output = mb_substr($buffer, 0, $safeLength);
$output = preg_replace('/file poster="/', 'file poster="'.$uid.'-', $output);
echo $output;
$buffer = mb_substr($buffer, $safeLength);
}
}
}
gzclose($fileHandle);
}
/**
* Sanitize filename for download
*/
private function sanitizeFilename(string $filename): string
{
return str_replace([',', ' ', '/', '\\'], '_', $filename);
}
}