From f6c18cdeacb0f57c44bf51422915086cd393397f Mon Sep 17 00:00:00 2001 From: DariusIII Date: Fri, 10 Jul 2026 21:31:19 +0200 Subject: [PATCH] Update views and related controllers --- app/Http/Controllers/AdultController.php | 18 +- app/Http/Controllers/BasePageController.php | 83 +++++ app/Http/Controllers/BooksController.php | 24 +- app/Http/Controllers/BrowseController.php | 63 ++-- app/Http/Controllers/CartController.php | 2 +- app/Http/Controllers/ConsoleController.php | 26 +- app/Http/Controllers/ContentController.php | 25 +- app/Http/Controllers/DetailsController.php | 14 - app/Http/Controllers/GamesController.php | 26 +- app/Http/Controllers/GetNzbController.php | 4 +- app/Http/Controllers/MovieController.php | 43 ++- app/Http/Controllers/MusicController.php | 33 +- app/Http/Controllers/MyMoviesController.php | 111 +++--- app/Http/Controllers/MyShowsController.php | 100 ++--- app/Http/Controllers/ProfileController.php | 10 +- app/Http/Controllers/RssController.php | 12 +- app/Http/Controllers/SearchController.php | 127 ++++--- app/Http/Controllers/SeriesController.php | 12 +- app/Models/UserMovie.php | 4 +- app/Models/UserSerie.php | 4 +- .../Releases/ReleaseBrowseService.php | 22 +- .../Releases/ReleaseSearchService.php | 27 +- resources/views/books/index.blade.php | 32 +- resources/views/browse/index.blade.php | 344 ++---------------- .../components/cover-release-list.blade.php | 100 +++++ .../cover-results-toolbar.blade.php | 37 ++ .../release-results-panel.blade.php | 81 +++++ .../components/release-results.blade.php | 271 ++++++++++++++ resources/views/console/index.blade.php | 93 +---- resources/views/details/index.blade.php | 41 +-- resources/views/games/index.blade.php | 97 +---- resources/views/music/index.blade.php | 97 +---- resources/views/search/index.blade.php | 249 +------------ tests/Feature/AdminContentControllerTest.php | 23 ++ .../AdminReleaseReportControllerTest.php | 10 +- tests/Unit/CoverBrowseComponentsTest.php | 39 ++ tests/Unit/ReleaseBrowseServiceTest.php | 13 + tests/Unit/ReleaseResultsComponentTest.php | 47 +++ .../Unit/ReleaseSearchServiceOrderingTest.php | 16 + tests/Unit/UserViewRequestInputTest.php | 101 +++++ 40 files changed, 1303 insertions(+), 1178 deletions(-) create mode 100644 resources/views/components/cover-release-list.blade.php create mode 100644 resources/views/components/cover-results-toolbar.blade.php create mode 100644 resources/views/components/release-results-panel.blade.php create mode 100644 resources/views/components/release-results.blade.php create mode 100644 tests/Unit/CoverBrowseComponentsTest.php create mode 100644 tests/Unit/ReleaseResultsComponentTest.php create mode 100644 tests/Unit/UserViewRequestInputTest.php diff --git a/app/Http/Controllers/AdultController.php b/app/Http/Controllers/AdultController.php index dc9951edf..dd2aa6771 100644 --- a/app/Http/Controllers/AdultController.php +++ b/app/Http/Controllers/AdultController.php @@ -32,24 +32,25 @@ class AdultController extends BasePageController 'title' => $mcat->title, ]; } - $category = $request->has('t') ? $request->input('t') : Category::XXX_ROOT; + $category = $this->integerInput($request, 't', Category::XXX_ROOT); if ($id && \in_array($id, Arr::pluck($mtmp, 'title'), true)) { $cat = Category::query() ->where('title', $id) ->where('root_categories_id', '=', Category::XXX_ROOT) ->first(['id']); - $category = $cat !== null ? $cat['id'] : Category::XXX_ROOT; + $category = $cat !== null ? (int) $cat['id'] : Category::XXX_ROOT; } $catarray = []; $catarray[] = $category; $ordering = $this->releaseBrowseService->getBrowseOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : ''; + $orderby = $this->resolveOrderBy($request, $ordering); - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); - $rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderby, -1, (array) ($this->userdata->categoryexclusions ?? []), -1); - $results = $this->paginate($rslt ?? [], isset($rslt[0]->_totalcount) ? $rslt[0]->_totalcount : 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_page'); + $offset = $this->paginationOffset($page, $perPage); + $rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, $perPage, $orderby, -1, (array) ($this->userdata->categoryexclusions ?? []), -1); + $results = $this->paginate($rslt ?? [], isset($rslt[0]->_totalcount) ? $rslt[0]->_totalcount : 0, $perPage, $page, $request->url(), $request->query()); if ((int) $category === -1) { $catname = 'All'; @@ -60,7 +61,7 @@ class AdultController extends BasePageController $orderByUrls = []; foreach ($ordering as $ordertype) { - $orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.'&ob='.$ordertype.'&offset=0'); + $orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.'&ob='.$ordertype); } $this->viewData = array_merge($this->viewData, [ @@ -69,6 +70,7 @@ class AdultController extends BasePageController 'categorytitle' => $id, 'catname' => $catname, 'ordering' => $ordering, + 'orderByUrls' => $orderByUrls, 'results' => $results, 'lastvisit' => $this->userdata['lastlogin'] ?? null, 'meta_title' => 'Browse XXX', diff --git a/app/Http/Controllers/BasePageController.php b/app/Http/Controllers/BasePageController.php index c1655ea0f..9ad7a4989 100644 --- a/app/Http/Controllers/BasePageController.php +++ b/app/Http/Controllers/BasePageController.php @@ -124,6 +124,89 @@ class BasePageController extends Controller return new LengthAwarePaginator($query, $totalCount, $items, $page, ['path' => $path, 'query' => $reqQuery]); } + protected function resolvePage(Request $request, string $key = 'page'): int + { + $page = $request->input($key, 1); + if (! is_scalar($page)) { + return 1; + } + + $page = (string) $page; + if ($page === '' || preg_match('/^\d+$/', $page) !== 1) { + return 1; + } + + return max(1, (int) $page); + } + + /** + * @param array $ordering + */ + protected function resolveOrderBy(Request $request, array $ordering, string $key = 'ob'): string + { + $orderByInput = $request->input($key, ''); + $orderBy = is_scalar($orderByInput) ? (string) $orderByInput : ''; + + return \in_array($orderBy, $ordering, true) ? $orderBy : ''; + } + + protected function paginationOffset(int $page, int $perPage): int + { + return ($page - 1) * $perPage; + } + + /** + * @param array $ordering + * @return array + */ + protected function buildOrderByUrls(array $ordering, string $basePath, string $prefix = 'orderby'): array + { + $orderByUrls = []; + foreach ($ordering as $orderType) { + $orderByUrls[$prefix.$orderType] = url($basePath.'?ob='.$orderType); + } + + return $orderByUrls; + } + + protected function scalarInput(Request $request, string $key, string $default = ''): string + { + $value = $request->input($key, $default); + + return is_scalar($value) ? (string) $value : $default; + } + + protected function integerInput(Request $request, string $key, int $default = 0): int + { + $value = $this->scalarInput($request, $key, (string) $default); + + return preg_match('/^-?\d+$/', $value) === 1 ? (int) $value : $default; + } + + /** + * @return array + */ + protected function arrayInput(Request $request, string $key): array + { + $value = $request->input($key, []); + + return \is_array($value) ? $value : []; + } + + protected function localReturnUrl(Request $request, string $fallback, string $key = 'from'): string + { + $from = $this->scalarInput($request, $key); + if ($from === '') { + return url($fallback); + } + + if (filter_var($from, FILTER_VALIDATE_URL) !== false) { + return parse_url($from, PHP_URL_HOST) === $request->getHost() ? $from : url($fallback); + } + + return url($from); + } + /** * Check if request is POST. */ diff --git a/app/Http/Controllers/BooksController.php b/app/Http/Controllers/BooksController.php index ab99a1572..1e2824ef5 100644 --- a/app/Http/Controllers/BooksController.php +++ b/app/Http/Controllers/BooksController.php @@ -34,27 +34,27 @@ class BooksController extends BasePageController 'title' => $bcat->title, ]; } - $category = $request->has('t') ? $request->input('t') : Category::BOOKS_ROOT; + $category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::BOOKS_ROOT) : Category::BOOKS_ROOT; if ($id && \in_array($id, Arr::pluck($btmp, 'title'), false)) { $cat = Category::query() ->where('title', $id) ->where('root_categories_id', '=', Category::BOOKS_ROOT) ->first(['id']); - $category = $cat !== null ? $cat['id'] : Category::BOOKS_ROOT; + $category = $cat !== null ? (int) $cat['id'] : Category::BOOKS_ROOT; } $catarray = []; - $catarray[] = $category; + $catarray[] = (int) $category; $ordering = $this->bookService->getBookOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : ''; + $orderby = $this->resolveOrderBy($request, $ordering); $books = []; - $pageInput = $request->input('page'); - $page = is_scalar($pageInput) && preg_match('/^\d+$/', (string) $pageInput) === 1 ? max(1, (int) $pageInput) : 1; - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); - $rslt = $this->bookService->getBookRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions); - $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); + $rslt = $this->bookService->getBookRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions); + $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query()); $maxwords = 50; foreach ($results as $result) { if (! empty($result->overview)) { @@ -67,9 +67,11 @@ class BooksController extends BasePageController $books[] = $result; } - $author = ($request->has('author') && ! empty($request->input('author'))) ? stripslashes($request->input('author')) : ''; + $authorInput = $this->scalarInput($request, 'author'); + $author = $authorInput !== '' ? stripslashes($authorInput) : ''; - $title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : ''; + $titleInput = $this->scalarInput($request, 'title'); + $title = $titleInput !== '' ? stripslashes($titleInput) : ''; $browseby_link = '&title='.$title.'&author='.$author; diff --git a/app/Http/Controllers/BrowseController.php b/app/Http/Controllers/BrowseController.php index 0d675e4c1..ca8a3129c 100644 --- a/app/Http/Controllers/BrowseController.php +++ b/app/Http/Controllers/BrowseController.php @@ -8,6 +8,7 @@ use App\Models\Category; use App\Models\RootCategory; use App\Services\Releases\ReleaseBrowseService; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; class BrowseController extends BasePageController { @@ -25,18 +26,12 @@ class BrowseController extends BasePageController public function index(Request $request): mixed { $ordering = $this->releaseBrowseService->getBrowseOrdering(); - $orderBy = $request->has('ob') && ! empty($request->input('ob')) ? $request->input('ob') : ''; - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); - - $rslt = $this->releaseBrowseService->getBrowseRange($page, [-1], $offset, config('nntmux.items_per_page'), $orderBy, -1, (array) $this->userdata->categoryexclusions, -1); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $orderBy = $this->resolveOrderBy($request, $ordering); + $page = $this->resolvePage($request); + $results = $this->getBrowsePaginator($request, $page, [-1], $orderBy); // Build order by URLs - $orderByUrls = []; - foreach ($ordering as $orderType) { - $orderByUrls['orderby'.$orderType] = url('browse/All?ob='.$orderType); - } + $orderByUrls = $this->buildOrderByUrls($ordering, 'browse/All'); $this->viewData = array_merge($this->viewData, [ 'category' => -1, @@ -57,12 +52,13 @@ class BrowseController extends BasePageController public function show(Request $request, string $parentCategory, string $id = 'All'): mixed { - $parentId = RootCategory::query()->where('title', $parentCategory)->value('id'); + $parentIdValue = RootCategory::query()->where('title', $parentCategory)->value('id'); + $parentId = $parentIdValue === null ? null : (int) $parentIdValue; $query = Category::query()->where('title', $id)->where('root_categories_id', $parentId); if ($id !== 'All') { $cat = $query->first(); - $category = $cat !== null ? $cat->id : -1; + $category = $cat !== null ? (int) $cat->id : -1; } else { $category = $parentId ?? -1; } @@ -73,12 +69,9 @@ class BrowseController extends BasePageController $catarray[] = $category; $ordering = $this->releaseBrowseService->getBrowseOrdering(); - $orderBy = $request->has('ob') && ! empty($request->input('ob')) ? $request->input('ob') : ''; - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); - - $rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderBy, -1, (array) $this->userdata->categoryexclusions, $grp); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $orderBy = $this->resolveOrderBy($request, $ordering); + $page = $this->resolvePage($request); + $results = $this->getBrowsePaginator($request, $page, $catarray, $orderBy, $grp); $covgroup = ''; $shows = false; @@ -126,14 +119,10 @@ class BrowseController extends BasePageController $orderByUrls = []; if ($id === 'All' && $parentCategory === 'All') { $meta_title = 'Browse '.$parentCategory.' releases'; - foreach ($ordering as $orderType) { - $orderByUrls['orderby'.$orderType] = url('browse/'.$parentCategory.'?ob='.$orderType); - } + $orderByUrls = $this->buildOrderByUrls($ordering, 'browse/'.$parentCategory); } else { $meta_title = 'Browse '.$parentCategory.' / '.$id.' releases'; - foreach ($ordering as $orderType) { - $orderByUrls['orderby'.$orderType] = url('browse/'.$parentCategory.'/'.$id.'?ob='.$orderType); - } + $orderByUrls = $this->buildOrderByUrls($ordering, 'browse/'.$parentCategory.'/'.$id); } $viewData = [ @@ -163,11 +152,14 @@ class BrowseController extends BasePageController public function group(Request $request): mixed { if ($request->has('g')) { - $group = $request->input('g'); - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); - $rslt = $this->releaseBrowseService->getBrowseRange($page, [-1], $offset, config('nntmux.items_per_page'), '', -1, (array) $this->userdata->categoryexclusions, $group); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $groupInput = $request->input('g'); + if (! is_scalar($groupInput)) { + return redirect()->back()->with('error', 'Group parameter is invalid'); + } + + $group = (string) $groupInput; + $page = $this->resolvePage($request); + $results = $this->getBrowsePaginator($request, $page, [-1], '', $group); $this->viewData = array_merge($this->viewData, [ 'results' => $results, @@ -184,4 +176,17 @@ class BrowseController extends BasePageController return redirect()->back()->with('error', 'Group parameter is required'); } + + /** + * @param array $categories + * @return LengthAwarePaginator + */ + private function getBrowsePaginator(Request $request, int $page, array $categories, string $orderBy, int|string $group = -1): LengthAwarePaginator + { + $perPage = (int) config('nntmux.items_per_page'); + $offset = $this->paginationOffset($page, $perPage); + $rslt = $this->releaseBrowseService->getBrowseRange($page, $categories, $offset, $perPage, $orderBy, -1, (array) $this->userdata->categoryexclusions, $group); + + return $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query()); + } } diff --git a/app/Http/Controllers/CartController.php b/app/Http/Controllers/CartController.php index cde34bf26..60d896a41 100644 --- a/app/Http/Controllers/CartController.php +++ b/app/Http/Controllers/CartController.php @@ -36,7 +36,7 @@ class CartController extends BasePageController */ public function store(Request $request): RedirectResponse|JsonResponse { - $guids = collect(explode(',', (string) $request->input('id'))) + $guids = collect(explode(',', $this->scalarInput($request, 'id'))) ->map(static fn (string $guid): string => trim($guid)) ->filter() ->unique() diff --git a/app/Http/Controllers/ConsoleController.php b/app/Http/Controllers/ConsoleController.php index 69db8f82c..c58f3c60e 100644 --- a/app/Http/Controllers/ConsoleController.php +++ b/app/Http/Controllers/ConsoleController.php @@ -40,26 +40,27 @@ class ConsoleController extends BasePageController 'title' => $ccat->title, ]; } - $category = $request->has('t') ? $request->input('t') : Category::GAME_ROOT; + $category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::GAME_ROOT) : Category::GAME_ROOT; if ($id && \in_array($id, Arr::pluck($ctmp, 'title'), false)) { $cat = Category::query() ->where('title', $id) ->where('root_categories_id', '=', Category::GAME_ROOT) ->first(['id']); - $category = $cat !== null ? $cat['id'] : Category::GAME_ROOT; + $category = $cat !== null ? (int) $cat['id'] : Category::GAME_ROOT; } $catarray = []; - $catarray[] = $category; + $catarray[] = (int) $category; $ordering = $this->consoleService->getConsoleOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : ''; - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); + $orderby = $this->resolveOrderBy($request, $ordering); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); $consoles = []; - $rslt = $this->consoleService->getConsoleRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions); - $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); + $rslt = $this->consoleService->getConsoleRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions); + $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query()); $maxwords = 50; foreach ($results as $result) { @@ -73,8 +74,10 @@ class ConsoleController extends BasePageController $consoles[] = $result; } - $platform = ($request->has('platform') && ! empty($request->input('platform'))) ? stripslashes($request->input('platform')) : ''; - $title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : ''; + $platformInput = $this->scalarInput($request, 'platform'); + $platform = $platformInput !== '' ? stripslashes($platformInput) : ''; + $titleInput = $this->scalarInput($request, 'title'); + $title = $titleInput !== '' ? stripslashes($titleInput) : ''; $genres = $gen->getGenres((string) GenreService::CONSOLE_TYPE, true); $tmpgnr = []; @@ -82,7 +85,8 @@ class ConsoleController extends BasePageController /** @var Genre $gn */ $tmpgnr[$gn->id] = $gn->title; } - $genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : ''; + $genreInput = $this->scalarInput($request, 'genre'); + $genre = isset($tmpgnr[$genreInput]) ? $genreInput : ''; if ((int) $category === -1) { $catname = 'All'; diff --git a/app/Http/Controllers/ContentController.php b/app/Http/Controllers/ContentController.php index 610cf1750..d8d440934 100644 --- a/app/Http/Controllers/ContentController.php +++ b/app/Http/Controllers/ContentController.php @@ -22,7 +22,7 @@ class ContentController extends BasePageController */ public function show(Request $request) { - $role = $this->userdata->role ?? 0; + $role = $this->contentRole(); /* The role column in the content table values are: * 1 = logged in users @@ -38,8 +38,8 @@ class ContentController extends BasePageController */ $isAdmin = \in_array($role, [UserRole::ADMIN->value, UserRole::MODERATOR->value], true); - $contentId = $request->input('id', 0); - $contentPage = $request->input('page', false); + $contentId = $this->integerInput($request, 'id'); + $contentPage = $this->scalarInput($request, 'page'); if ($contentId === 0 && $contentPage === 'content') { // Show all content except front page @@ -48,7 +48,7 @@ class ContentController extends BasePageController $meta_title = 'Contents page'; $meta_keywords = 'contents'; $meta_description = 'This is the contents page.'; - } elseif ($contentId !== 0 && $contentPage !== false) { + } elseif ($contentId !== 0 && $contentPage !== '') { // Show specific content by ID $contentItem = $this->getContentById($contentId, $role); $content = $contentItem ? [$contentItem] : []; @@ -82,6 +82,23 @@ class ContentController extends BasePageController return view('content.index', $this->viewData); } + private function contentRole(): int + { + if (! isset($this->userdata)) { + return Content::ROLE_EVERYONE; + } + + if ($this->userdata->hasRole('Admin')) { + return UserRole::ADMIN->value; + } + + if ($this->userdata->hasRole('Moderator')) { + return UserRole::MODERATOR->value; + } + + return Content::ROLE_LOGGED_IN; + } + /** * Get all active content ordered by type and ordinal. * diff --git a/app/Http/Controllers/DetailsController.php b/app/Http/Controllers/DetailsController.php index d32a4924d..0905c077f 100644 --- a/app/Http/Controllers/DetailsController.php +++ b/app/Http/Controllers/DetailsController.php @@ -8,12 +8,9 @@ use App\Models\DnzbFailure; use App\Models\Predb; use App\Models\Release; use App\Models\ReleaseComment; -use App\Models\ReleaseFile; -use App\Models\ReleaseNfo; use App\Models\ReleaseRegex; use App\Models\ReleaseReport; use App\Models\Settings; -use App\Models\UserDownload; use App\Models\Video; use App\Services\AnidbService; use App\Services\BookService; @@ -63,9 +60,6 @@ class DetailsController extends BasePageController return redirect()->route('details', ['guid' => $guid])->with('success', 'Comment posted successfully!'); } - $nfoData = ReleaseNfo::getReleaseNfo($data['id']); - /** @var ReleaseNfo|null $nfoData */ - $nfo = $nfoData ? $nfoData->nfo : null; $reVideo = $this->releaseExtraService->getVideo($data['id']); $reAudio = $this->releaseExtraService->getAudio($data['id']); $reSubs = $this->releaseExtraService->getSubs($data['id']); @@ -93,8 +87,6 @@ class DetailsController extends BasePageController ->where('response', '!=', '') ->orderByDesc('responded_at') ->get(); - $downloadedBy = UserDownload::query()->with('user')->where('releases_id', $data['id'])->get(['users_id']); - $showInfo = ''; if ($data['videos_id'] > 0) { $showInfo = Video::getByVideoID($data['videos_id']); @@ -166,15 +158,11 @@ class DetailsController extends BasePageController $pre = Predb::getForRelease($data['predb_id']); - $releasefiles = ReleaseFile::getReleaseFiles($data['id']); - $this->viewData = array_merge($this->viewData, [ - 'releasefiles' => $releasefiles, 'release' => $data, 'reVideo' => $reVideo, 'reAudio' => $reAudio, 'reSubs' => $reSubs, - 'nfo' => $nfo, 'show' => $showInfo, 'movie' => $mov, 'anidb' => $AniDBAPIArray, @@ -184,7 +172,6 @@ class DetailsController extends BasePageController 'book' => $book, 'predb' => $pre, 'comments' => $comments, - 'files' => $releasefiles, 'searchname' => getSimilarName($data['searchname']), 'similars' => $similars !== false ? $similars : [], 'privateprofiles' => config('nntmux_settings.private_profiles'), @@ -196,7 +183,6 @@ class DetailsController extends BasePageController 'allReportReasons' => $allReportReasons, 'publicReportResponses' => $publicReportResponses, 'regex' => $releaseRegex, - 'downloadedby' => $downloadedBy, 'meta_title' => 'View NZB', 'meta_keywords' => 'view,nzb,description,details', 'meta_description' => 'View NZB for '.$data['searchname'], diff --git a/app/Http/Controllers/GamesController.php b/app/Http/Controllers/GamesController.php index eb1516ee2..12637aa65 100644 --- a/app/Http/Controllers/GamesController.php +++ b/app/Http/Controllers/GamesController.php @@ -26,21 +26,24 @@ class GamesController extends BasePageController $ctmp[$ccat['id']] = $ccat; } $category = Category::PC_GAMES; - if ($request->has('t') && isset($ctmp[$request->input('t')])) { - $category = $request->input('t') + 0; + $categoryInput = $this->scalarInput($request, 't'); + if ($categoryInput !== '' && isset($ctmp[$categoryInput])) { + $category = (int) $categoryInput; } $catarray = []; $catarray[] = $category; - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; + $page = $this->resolvePage($request); $ordering = $games->getGamesOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : ''; - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); - $rslt = $games->getGamesRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, '', (array) $this->userdata->categoryexclusions); - $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); + $orderby = $this->resolveOrderBy($request, $ordering); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); + $rslt = $games->getGamesRange($page, $catarray, $offset, $perPage, $orderby, '', (array) $this->userdata->categoryexclusions); + $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query()); - $title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : ''; + $titleInput = $this->scalarInput($request, 'title'); + $title = $titleInput !== '' ? stripslashes($titleInput) : ''; $genres = $gen->getGenres((string) GenreService::GAME_TYPE, true); $tmpgnr = []; @@ -51,9 +54,12 @@ class GamesController extends BasePageController $years = range(1903, date('Y') + 1); rsort($years); - $year = ($request->has('year') && \in_array($request->input('year'), $years, true)) ? $request->input('year') : ''; + $yearInput = $this->scalarInput($request, 'year'); + $yearValue = is_numeric($yearInput) ? (int) $yearInput : null; + $year = $yearValue !== null && \in_array($yearValue, $years, true) ? $yearValue : ''; - $genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : ''; + $genreInput = $this->scalarInput($request, 'genre'); + $genre = isset($tmpgnr[$genreInput]) ? $genreInput : ''; if ((int) $category === -1) { $catname = 'All'; diff --git a/app/Http/Controllers/GetNzbController.php b/app/Http/Controllers/GetNzbController.php index fed98bc91..6b54fb502 100644 --- a/app/Http/Controllers/GetNzbController.php +++ b/app/Http/Controllers/GetNzbController.php @@ -195,9 +195,9 @@ class GetNzbController extends BasePageController */ private function validateAndSanitizeId(Request $request): string|Response { - $id = $request->input('id'); + $id = $this->scalarInput($request, 'id'); - if (empty($id)) { + if ($id === '') { return showApiError(200, 'Parameter id is required'); } diff --git a/app/Http/Controllers/MovieController.php b/app/Http/Controllers/MovieController.php index 3572adeef..4f9fb0b30 100644 --- a/app/Http/Controllers/MovieController.php +++ b/app/Http/Controllers/MovieController.php @@ -36,26 +36,23 @@ class MovieController extends BasePageController return ['id' => $mcat->id, 'title' => $mcat->title]; }); - $category = $request->has('imdb') ? -1 : ($request->input('t', Category::MOVIE_ROOT)); + $category = $request->has('imdb') ? -1 : $this->integerInput($request, 't', Category::MOVIE_ROOT); if ($id && $moviecats->pluck('title')->contains($id)) { $cat = Category::where(['title' => $id, 'root_categories_id' => Category::MOVIE_ROOT])->first(['id']); - $category = $cat->id ?? Category::MOVIE_ROOT; + $category = $cat !== null ? (int) $cat->id : Category::MOVIE_ROOT; } $catarray = $category !== -1 ? [$category] : []; - $page = (int) $request->input('page', 1); - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); - - $orderby = $request->input('ob', ''); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); $ordering = $this->movieBrowseService->getMovieOrdering(); - if (! in_array($orderby, $ordering, true)) { - $orderby = ''; - } + $orderby = $this->resolveOrderBy($request, $ordering); - $rslt = $this->movieBrowseService->getMovieRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, -1, (array) $this->userdata->categoryexclusions); + $rslt = $this->movieBrowseService->getMovieRange($page, $catarray, $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions); $totalCount = $rslt->isNotEmpty() ? ($rslt[0]->_totalcount ?? 0) : 0; - $results = $this->paginate($rslt ?? [], $totalCount, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); + $results = $this->paginate($rslt ?? [], $totalCount, $perPage, $page, $request->url(), $request->query()); $movies = $results->map(function ($result) { $result->genre = makeFieldLinks($result, 'genre', 'movies'); @@ -71,6 +68,11 @@ class MovieController extends BasePageController $years = range(1903, now()->addYear()->year); rsort($years); + $genres = $this->movieBrowseService->getGenres(); + $ratingInput = $this->integerInput($request, 'rating', 0); + $genreInput = $this->scalarInput($request, 'genre'); + $yearInput = $this->integerInput($request, 'year', 0); + $catname = $category === -1 ? 'All' : Category::find($category) ?? 'All'; $this->viewData = array_merge($this->viewData, [ @@ -79,15 +81,15 @@ class MovieController extends BasePageController 'catlist' => $moviecats, 'category' => $category, 'categorytitle' => $id, - 'title' => stripslashes((string) ($request->input('title') ?? '')), - 'actors' => stripslashes((string) ($request->input('actors') ?? '')), - 'director' => stripslashes((string) ($request->input('director') ?? '')), + 'title' => stripslashes($this->scalarInput($request, 'title')), + 'actors' => stripslashes($this->scalarInput($request, 'actors')), + 'director' => stripslashes($this->scalarInput($request, 'director')), 'ratings' => range(1, 9), - 'rating' => $request->input('rating', ''), - 'genres' => $this->movieBrowseService->getGenres(), - 'genre' => $request->input('genre', ''), + 'rating' => \in_array($ratingInput, range(1, 9), true) ? $ratingInput : '', + 'genres' => $genres, + 'genre' => \in_array($genreInput, $genres, true) ? $genreInput : '', 'years' => $years, - 'year' => $request->input('year', ''), + 'year' => \in_array($yearInput, $years, true) ? $yearInput : '', 'catname' => $catname, 'resultsadd' => $movies, 'results' => $results, @@ -162,8 +164,9 @@ class MovieController extends BasePageController */ public function showTrailer(Request $request) { - if ($request->has('id') && ctype_digit($request->input('id'))) { - $mov = $this->movieService->getMovieInfo($request->input('id')); + $movieId = $this->scalarInput($request, 'id'); + if ($movieId !== '' && ctype_digit($movieId)) { + $mov = $this->movieService->getMovieInfo($movieId); if (! $mov) { return response()->json(['message' => 'There is no trailer for this movie.'], 404); diff --git a/app/Http/Controllers/MusicController.php b/app/Http/Controllers/MusicController.php index bd10690d7..33d598aec 100644 --- a/app/Http/Controllers/MusicController.php +++ b/app/Http/Controllers/MusicController.php @@ -39,31 +39,33 @@ class MusicController extends BasePageController ]; } - $category = $request->has('t') ? $request->input('t') : Category::MUSIC_ROOT; + $category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::MUSIC_ROOT) : Category::MUSIC_ROOT; if ($id && \in_array($id, Arr::pluck($mtmp, 'title'), true)) { $cat = Category::query() ->where('title', $id) ->where('root_categories_id', '=', Category::MUSIC_ROOT) ->first(['id']); - $category = $cat !== null ? $cat['id'] : Category::MUSIC_ROOT; + $category = $cat !== null ? (int) $cat['id'] : Category::MUSIC_ROOT; } $catarray = []; - $catarray[] = $category; + $catarray[] = (int) $category; - $pageInput = $request->input('page'); - $page = is_scalar($pageInput) && preg_match('/^\d+$/', (string) $pageInput) === 1 ? max(1, (int) $pageInput) : 1; - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); $ordering = $this->musicService->getMusicOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : ''; + $orderby = $this->resolveOrderBy($request, $ordering); $musics = []; - $rslt = $this->musicService->getMusicRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); + $rslt = $this->musicService->getMusicRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions); + $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query()); - $artist = ($request->has('artist') && ! empty($request->input('artist'))) ? stripslashes($request->input('artist')) : ''; + $artistInput = $this->scalarInput($request, 'artist'); + $artist = $artistInput !== '' ? stripslashes($artistInput) : ''; - $title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : ''; + $titleInput = $this->scalarInput($request, 'title'); + $title = $titleInput !== '' ? stripslashes($titleInput) : ''; $genres = $this->genreService->getGenres((string) GenreService::MUSIC_TYPE, true); $tmpgnr = []; @@ -74,15 +76,18 @@ class MusicController extends BasePageController foreach ($results as $result) { $res = $result; - $result->genre = $tmpgnr[$res->genres_id]; + $result->genre = $tmpgnr[$res->genres_id] ?? ''; $musics[] = $result; } - $genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : ''; + $genreInput = $this->scalarInput($request, 'genre'); + $genre = isset($tmpgnr[$genreInput]) ? $genreInput : ''; $years = range(1950, date('Y') + 1); rsort($years); - $year = ($request->has('year') && \in_array($request->input('year'), $years, true)) ? $request->input('year') : ''; + $yearInput = $this->scalarInput($request, 'year'); + $yearValue = is_numeric($yearInput) ? (int) $yearInput : null; + $year = $yearValue !== null && \in_array($yearValue, $years, true) ? $yearValue : ''; if ((int) $category === -1) { $catname = 'All'; diff --git a/app/Http/Controllers/MyMoviesController.php b/app/Http/Controllers/MyMoviesController.php index 52a973620..44e825858 100644 --- a/app/Http/Controllers/MyMoviesController.php +++ b/app/Http/Controllers/MyMoviesController.php @@ -28,14 +28,11 @@ class MyMoviesController extends BasePageController public function show(Request $request): mixed { - $action = $request->input('id') ?? ''; - $imdbid = $request->input('imdb') ?? ''; + $action = $this->scalarInput($request, 'id'); + $imdbid = $this->scalarInput($request, 'imdb'); + $from = $this->localReturnUrl($request, '/mymovies'); - if ($request->has('from')) { - $this->viewData['from'] = url($request->input('from')); - } else { - $this->viewData['from'] = url('/mymovies'); - } + $this->viewData['from'] = $from; switch ($action) { case 'delete': @@ -44,13 +41,8 @@ class MyMoviesController extends BasePageController return redirect()->to('/mymovies'); } UserMovie::delMovie($this->userdata->id, $imdbid); - if ($request->has('from')) { - header('Location:'.url($request->input('from'))); - } else { - return redirect()->to('/mymovies'); - } - break; + return redirect()->to($from); case 'add': case 'doadd': $movie = UserMovie::getMovie($this->userdata->id, $imdbid); @@ -64,24 +56,13 @@ class MyMoviesController extends BasePageController } if ($action === 'doadd') { - $category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : []; + $category = $this->selectedCategoryIds($request, $this->movieCategories(true)); UserMovie::addMovie($this->userdata->id, $imdbid, $category); - if ($request->has('from')) { - return redirect()->to($request->input('from')); - } - return redirect()->to('/mymovies'); + return redirect()->to($from); } - $tmpcats = Category::getChildren(Category::MOVIE_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - // If MOVIE WEB-DL categorization is disabled, don't include it as an option - if ((int) $c['id'] === Category::MOVIE_WEBDL && (int) Settings::settingValue('catwebdl') === 0) { - continue; - } - $categories[$c['id']] = $c['title']; - } + $categories = $this->movieCategories(true); $this->viewData['type'] = 'add'; $this->viewData['cat_ids'] = array_keys($categories); $this->viewData['cat_names'] = $categories; @@ -102,20 +83,13 @@ class MyMoviesController extends BasePageController } if ($action === 'doedit') { - $category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : []; + $category = $this->selectedCategoryIds($request, $this->movieCategories(false)); UserMovie::updateMovie($this->userdata->id, $imdbid, $category); - if ($request->has('from')) { - return redirect()->to($request->input('from')); - } - return redirect()->to('mymovies'); + return redirect()->to($from); } - $tmpcats = Category::getChildren(Category::MOVIE_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - $categories[$c['id']] = $c['title']; - } + $categories = $this->movieCategories(false); $this->viewData['type'] = 'edit'; $this->viewData['cat_ids'] = array_keys($categories); @@ -135,19 +109,19 @@ class MyMoviesController extends BasePageController $meta_keywords = 'search,add,to,cart,nzb,description,details'; $meta_description = 'Browse Your Movies'; - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - - $offset = ($page - 1) * (int) config('nntmux.items_per_cover_page'); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_cover_page'); + $offset = $this->paginationOffset($page, $perPage); $movies = UserMovie::getMovies($this->userdata->id); /** @var array $categories */ - $categories = $movie = []; + $categories = $this->movieCategories(false); foreach ($movies as $moviek => $movie) { $showcats = explode('|', $movie['categories']); if (\count($showcats) > 0) { $catarr = []; foreach ($showcats as $scat) { - if (! empty($scat)) { + if (! empty($scat) && isset($categories[$scat])) { $catarr[] = $categories[$scat]; } } @@ -155,13 +129,14 @@ class MyMoviesController extends BasePageController } else { $movie['categoryNames'] = ''; } + + $movies[$moviek] = $movie; } - $ordering = $request->input('ob', ''); + $ordering = $this->movieBrowseService->getMovieOrdering(); + $orderby = $this->resolveOrderBy($request, $ordering); - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - - $results = $this->movieBrowseService->getMovieRange($page, [], $offset, (int) config('nntmux.items_per_cover_page'), $ordering, -1, (array) $this->userdata->categoryexclusions); + $results = $this->movieBrowseService->getMovieRange($page, [], $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions); $this->viewData['covgroup'] = ''; @@ -171,9 +146,10 @@ class MyMoviesController extends BasePageController $this->viewData['lastvisit'] = $this->userdata->lastlogin; $this->viewData['results'] = $results; + $this->viewData['resultsadd'] = $results; $this->viewData['movies'] = true; /** @var view-string $browseView */ - $browseView = 'browse'; + $browseView = 'browse.index'; $this->viewData['content'] = view($browseView, $this->viewData)->render(); $this->viewData = array_merge($this->viewData, compact('title', 'meta_title', 'meta_keywords', 'meta_description')); @@ -186,11 +162,7 @@ class MyMoviesController extends BasePageController $meta_keywords = 'search,add,to,cart,nzb,description,details'; $meta_description = 'Manage Your Movies'; - $tmpcats = Category::getChildren(Category::MOVIE_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - $categories[$c['id']] = $c['title']; - } + $categories = $this->movieCategories(false); $movies = UserMovie::getMovies($this->userdata->id); $results = []; @@ -218,7 +190,38 @@ class MyMoviesController extends BasePageController return $this->pagerender(); } - // Fallback return in case no case matches - return redirect()->to('/mymovies'); + } + + /** + * @return array + */ + private function movieCategories(bool $excludeDisabledWebdl): array + { + $categories = []; + foreach (Category::getChildren(Category::MOVIE_ROOT) as $category) { + if ($excludeDisabledWebdl && (int) $category['id'] === Category::MOVIE_WEBDL && (int) Settings::settingValue('catwebdl') === 0) { + continue; + } + + $categories[(int) $category['id']] = (string) $category['title']; + } + + return $categories; + } + + /** + * @param array $allowedCategories + * @return array + */ + private function selectedCategoryIds(Request $request, array $allowedCategories): array + { + $selected = []; + foreach ($this->arrayInput($request, 'category') as $categoryId) { + if (is_scalar($categoryId) && preg_match('/^\d+$/', (string) $categoryId) === 1 && isset($allowedCategories[(int) $categoryId])) { + $selected[] = (int) $categoryId; + } + } + + return array_values(array_unique($selected)); } } diff --git a/app/Http/Controllers/MyShowsController.php b/app/Http/Controllers/MyShowsController.php index ff8115c97..d3bae3689 100644 --- a/app/Http/Controllers/MyShowsController.php +++ b/app/Http/Controllers/MyShowsController.php @@ -23,14 +23,11 @@ class MyShowsController extends BasePageController public function show(Request $request): mixed { - $action = $request->input('action') ?? ''; - $videoId = $request->input('id') ?? ''; + $action = $this->scalarInput($request, 'action'); + $videoId = $this->scalarInput($request, 'id'); + $from = $this->localReturnUrl($request, '/myshows'); - if ($request->has('from')) { - $this->viewData['from'] = url($request->input('from')); - } else { - $this->viewData['from'] = url('/myshows'); - } + $this->viewData['from'] = $from; switch ($action) { case 'delete': @@ -40,13 +37,8 @@ class MyShowsController extends BasePageController } UserSerie::delShow($this->userdata->id, $videoId); - if ($request->has('from')) { - header('Location:'.url($request->input('from'))); - } else { - return redirect()->to('myshows'); - } - break; + return redirect()->to($from); case 'add': case 'doadd': $show = UserSerie::getShow($this->userdata->id, $videoId); @@ -60,24 +52,13 @@ class MyShowsController extends BasePageController } if ($action === 'doadd') { - $category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : []; + $category = $this->selectedCategoryIds($request, $this->showCategories(true)); UserSerie::addShow($this->userdata->id, $videoId, $category); - if ($request->has('from')) { - return redirect()->to($request->input('from')); - } - return redirect()->to('myshows'); + return redirect()->to($from); } - $tmpcats = Category::getChildren(Category::TV_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - // If TV WEB-DL categorization is disabled, don't include it as an option - if ((int) $c['id'] === Category::TV_WEBDL && (int) Settings::settingValue('catwebdl') === 0) { - continue; - } - $categories[$c['id']] = $c['title']; - } + $categories = $this->showCategories(true); $this->viewData['type'] = 'add'; $this->viewData['cat_ids'] = array_keys($categories); $this->viewData['cat_names'] = $categories; @@ -98,20 +79,13 @@ class MyShowsController extends BasePageController } if ($action === 'doedit') { - $category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : []; + $category = $this->selectedCategoryIds($request, $this->showCategories(false)); UserSerie::updateShow($this->userdata->id, $videoId, $category); - if ($request->has('from')) { - return redirect()->to($request->input('from')); - } - return redirect()->to('myshows'); + return redirect()->to($from); } - $tmpcats = Category::getChildren(Category::TV_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - $categories[$c['id']] = $c['title']; - } + $categories = $this->showCategories(false); $this->viewData['type'] = 'edit'; $this->viewData['cat_ids'] = array_keys($categories); @@ -131,11 +105,7 @@ class MyShowsController extends BasePageController $meta_keywords = 'search,add,to,cart,nzb,description,details'; $meta_description = 'Manage Your Shows'; - $tmpcats = Category::getChildren(Category::TV_ROOT); - $categories = []; - foreach ($tmpcats as $c) { - $categories[$c['id']] = $c['title']; - } + $categories = $this->showCategories(false); $shows = UserSerie::getShows($this->userdata->id); $results = []; @@ -165,8 +135,6 @@ class MyShowsController extends BasePageController return $this->pagerender(); } - // Fallback return in case no case matches - return redirect()->to('/myshows'); } /** @@ -181,14 +149,15 @@ class MyShowsController extends BasePageController $shows = UserSerie::getShows($this->userdata->id); - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_page'); + $offset = $this->paginationOffset($page, $perPage); $ordering = $this->releaseBrowseService->getBrowseOrdering(); - $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : ''; + $orderby = $this->resolveOrderBy($request, $ordering); $browseCount = $shows ? $shows->count() : 0; - $rslt = $this->releaseBrowseService->getShowsRange($shows ?? [], $offset, config('nntmux.items_per_page'), $orderby, -1, (array) $this->userdata->categoryexclusions); - $results = $this->paginate($rslt ?? [], $browseCount, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $rslt = $this->releaseBrowseService->getShowsRange($shows ?? [], $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions); + $results = $this->paginate($rslt ?? [], $browseCount, $perPage, $page, $request->url(), $request->query()); $this->viewData['covgroup'] = ''; @@ -205,4 +174,37 @@ class MyShowsController extends BasePageController return $this->pagerender(); } + + /** + * @return array + */ + private function showCategories(bool $excludeDisabledWebdl): array + { + $categories = []; + foreach (Category::getChildren(Category::TV_ROOT) as $category) { + if ($excludeDisabledWebdl && (int) $category['id'] === Category::TV_WEBDL && (int) Settings::settingValue('catwebdl') === 0) { + continue; + } + + $categories[(int) $category['id']] = (string) $category['title']; + } + + return $categories; + } + + /** + * @param array $allowedCategories + * @return array + */ + private function selectedCategoryIds(Request $request, array $allowedCategories): array + { + $selected = []; + foreach ($this->arrayInput($request, 'category') as $categoryId) { + if (is_scalar($categoryId) && preg_match('/^\d+$/', (string) $categoryId) === 1 && isset($allowedCategories[(int) $categoryId])) { + $selected[] = (int) $categoryId; + } + } + + return array_values(array_unique($selected)); + } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index fc2950d50..d16fdfe33 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -41,8 +41,10 @@ class ProfileController extends BasePageController $publicView = false; if ($privileged || ! $privateProfiles) { - $altID = ($request->has('id') && (int) $request->input('id') >= 0) ? (int) $request->input('id') : false; - $altUsername = ($request->has('name') && $request->input('name') !== '') ? $request->input('name') : false; + $altIdInput = $this->scalarInput($request, 'id'); + $altID = $altIdInput !== '' && preg_match('/^\d+$/', $altIdInput) === 1 ? (int) $altIdInput : false; + $altUsername = $this->scalarInput($request, 'name'); + $altUsername = $altUsername !== '' ? $altUsername : false; // If both 'id' and 'name' are specified, 'id' should take precedence. if ($altID === false && $altUsername !== false) { @@ -252,9 +254,9 @@ class ProfileController extends BasePageController */ public function destroy(Request $request, GdprErasureService $erasureService): Application|View|Factory|RedirectResponse|\Illuminate\Contracts\Foundation\Application { - $userId = $request->input('id'); + $userId = $this->integerInput($request, 'id'); - if ($userId !== null && (int) $userId === $this->userdata->id && ! $this->userdata->hasRole('Admin')) { + if ($userId > 0 && $userId === $this->userdata->id && ! $this->userdata->hasRole('Admin')) { $user = User::find($userId); if ($user === null) { return redirect()->to('profile'); diff --git a/app/Http/Controllers/RssController.php b/app/Http/Controllers/RssController.php index 8b9b8347a..b6fd306f3 100644 --- a/app/Http/Controllers/RssController.php +++ b/app/Http/Controllers/RssController.php @@ -156,7 +156,8 @@ class RssController extends BasePageController */ public function categoryFeedRss(Request $request) { - if ($request->missing('id')) { + $categoryInput = $this->scalarInput($request, 'id'); + if ($categoryInput === '') { return response()->json(['error' => 'Category ID is missing'], 403); } @@ -165,7 +166,14 @@ class RssController extends BasePageController return $user; } - $categoryId = explode(',', $request->input('id')); + $categoryId = array_values(array_filter( + array_map('trim', explode(',', $categoryInput)), + static fn (string $categoryId): bool => preg_match('/^-?\d+$/', $categoryId) === 1 + )); + if ($categoryId === []) { + return response()->json(['error' => 'Category ID is missing'], 403); + } + [$userShow, $userAnidb, $userAirDate, $userNum, $userLimit, $outputXML] = $this->parseCommonRssParams($request); $relData = $this->rss->getRss($categoryId, $userShow, $userAnidb, $user['user_id'], $userAirDate, $userLimit, $userNum); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index e762fcdd7..0563d71c1 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -5,11 +5,12 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Models\Category; -use App\Models\UsenetGroup; use App\Services\Releases\ReleaseBrowseService; use App\Services\Releases\ReleaseSearchService; use App\Services\Search\Contracts\SearchServiceInterface; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\Cache; class SearchController extends BasePageController { @@ -44,9 +45,10 @@ class SearchController extends BasePageController } $ordering = $this->releaseBrowseService->getBrowseOrdering(); - $orderBy = ($request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : ''); - $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $offset = ($page - 1) * config('nntmux.items_per_page'); + $orderBy = $this->resolveOrderBy($request, $ordering); + $page = $this->resolvePage($request); + $perPage = (int) config('nntmux.items_per_page'); + $offset = $this->paginationOffset($page, $perPage); $subject = ''; $search = ''; @@ -58,15 +60,15 @@ class SearchController extends BasePageController $searchString = []; switch (true) { case $request->filled('subject'): - $searchString['searchname'] = (string) $request->input('subject'); + $searchString['searchname'] = $this->scalarInput($request, 'subject'); $subject = $searchString['searchname']; break; case $request->filled('id'): - $searchString['searchname'] = (string) $request->input('id'); + $searchString['searchname'] = $this->scalarInput($request, 'id'); $id = $searchString['searchname']; break; case $request->filled('search'): - $searchString['searchname'] = (string) $request->input('search'); + $searchString['searchname'] = $this->scalarInput($request, 'search'); $search = $searchString['searchname']; break; default: @@ -88,14 +90,14 @@ class SearchController extends BasePageController -1, -1, $offset, - (int) config('nntmux.items_per_page'), + $perPage, $orderBy, -1, $this->userdata->categoryexclusions ?? [], 'basic', $categoryID); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, $perPage, $page, $request->url(), $request->query()); $category = $categoryID; } else { $orderByUrls = []; @@ -117,32 +119,32 @@ class SearchController extends BasePageController ]; foreach ($searchVars as $searchVarKey => $searchVar) { - $searchVars[$searchVarKey] = ($request->has($searchVarKey) ? (string) $request->input($searchVarKey) : ''); + $searchVars[$searchVarKey] = $this->scalarInput($request, $searchVarKey); } // Map new form field names to old internal names if ($request->has('minage')) { - $searchVars['searchadvdaysnew'] = (string) $request->input('minage'); + $searchVars['searchadvdaysnew'] = $this->scalarInput($request, 'minage'); } if ($request->has('maxage')) { - $searchVars['searchadvdaysold'] = (string) $request->input('maxage'); + $searchVars['searchadvdaysold'] = $this->scalarInput($request, 'maxage'); } if ($request->has('group')) { - $searchVars['searchadvgroups'] = (string) $request->input('group'); + $searchVars['searchadvgroups'] = $this->scalarInput($request, 'group'); } if ($request->has('minsize')) { - $searchVars['searchadvsizefrom'] = (string) $request->input('minsize'); + $searchVars['searchadvsizefrom'] = $this->scalarInput($request, 'minsize'); } if ($request->has('maxsize')) { - $searchVars['searchadvsizeto'] = (string) $request->input('maxsize'); + $searchVars['searchadvsizeto'] = $this->scalarInput($request, 'maxsize'); } // Map basic search field to advanced search when in advanced mode if ($request->has('search') && $searchType === 'advanced') { - $searchVars['searchadvr'] = (string) $request->input('search'); + $searchVars['searchadvr'] = $this->scalarInput($request, 'search'); } // Map basic category field to advanced category when in advanced mode if ($request->has('t') && $searchType === 'advanced') { - $searchVars['searchadvcat'] = (string) $request->input('t'); + $searchVars['searchadvcat'] = implode(',', $this->resolveCategoryIdsFromRequest($request)); } $searchVars['selectedgroup'] = $searchVars['searchadvgroups']; @@ -150,22 +152,6 @@ class SearchController extends BasePageController $searchVars['selectedsizefrom'] = $searchVars['searchadvsizefrom']; $searchVars['selectedsizeto'] = $searchVars['searchadvsizeto']; - // Get spell correction suggestions if we have a search query but few/no results - $spellSuggestion = null; - $searchQuery = $search ?: $searchVars['searchadvr']; - if (! empty($searchQuery) && $this->searchService->isSuggestEnabled()) { - // Get suggestions from search service - $suggestions = $this->searchService->suggest($searchQuery); - if (! empty($suggestions)) { - // Sort by doc count descending to get best suggestion - usort($suggestions, fn ($a, $b) => $b['docs'] - $a['docs']); - // Only show suggestion if it's different from the query - if ($suggestions[0]['suggest'] !== $searchQuery) { - $spellSuggestion = $suggestions[0]['suggest']; - } - } - } - if ($searchType !== 'basic' && $request->missing('id') && $request->missing('subject') && $request->anyFilled(['searchadvr', 'searchadvsubject', 'searchadvfilename', 'searchadvposter', 'search'])) { $orderByString = ''; foreach ($searchVars as $searchVarKey => $searchVar) { @@ -173,7 +159,7 @@ class SearchController extends BasePageController } $orderByString = ltrim($orderByString, '&'); if ($request->filled('t')) { - $orderByString .= '&t='.urlencode((string) $request->input('t')); + $orderByString .= '&t='.urlencode(implode(',', $this->resolveCategoryIdsFromRequest($request))); } $orderByUrls = []; @@ -196,7 +182,7 @@ class SearchController extends BasePageController ($searchVars['searchadvdaysnew'] === '' ? -1 : $searchVars['searchadvdaysnew']), ($searchVars['searchadvdaysold'] === '' ? -1 : $searchVars['searchadvdaysold']), $offset, - (int) config('nntmux.items_per_page'), + $perPage, $orderBy, -1, $this->userdata->categoryexclusions ?? [], @@ -204,9 +190,12 @@ class SearchController extends BasePageController $this->resolveCategoryIdsFromRequest($request) ); - $results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); + $results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, $perPage, $page, $request->url(), $request->query()); } + $suggestEnabled = $this->searchService->isSuggestEnabled(); + $spellSuggestion = $this->resolveSpellSuggestion($search ?: $searchVars['searchadvr'], $results, $suggestEnabled); + $this->viewData = array_merge($this->viewData, $searchVars, $orderByUrls, [ 'subject' => $subject, 'search' => $search, @@ -214,21 +203,16 @@ class SearchController extends BasePageController 'category' => $category, 'covgroup' => '', 'lastvisit' => $lastvisit, - 'sizelist' => [ - -1 => '--Select--', 1 => '100MB', 2 => '250MB', 3 => '500MB', 4 => '1GB', 5 => '2GB', - 6 => '3GB', 7 => '4GB', 8 => '8GB', 9 => '16GB', 10 => '32GB', 11 => '64GB', - ], 'results' => $results, 'sadvanced' => $searchType !== 'basic', - 'grouplist' => UsenetGroup::getGroupsForSelect(), - 'catlist' => Category::getForSelect(), + 'catlist' => $this->getCategorySelectOptions(), 'meta_title' => 'Search Nzbs', 'meta_keywords' => 'search,nzb,description,details', 'meta_description' => 'Search for Nzbs', // Search enhanced features 'spellSuggestion' => $spellSuggestion, 'autocompleteEnabled' => $this->searchService->isAutocompleteEnabled(), - 'suggestEnabled' => $this->searchService->isSuggestEnabled(), + 'suggestEnabled' => $suggestEnabled, ]); return view('search.index', $this->viewData); @@ -239,12 +223,63 @@ class SearchController extends BasePageController */ private function resolveCategoryIdsFromRequest(Request $request): array { - $raw = $request->input('t', $request->input('searchadvcat', '')); + $raw = $this->scalarInput($request, 't', $this->scalarInput($request, 'searchadvcat')); - if ($raw === null || $raw === '' || $raw === '-1') { + if ($raw === '' || $raw === '-1') { return [-1]; } - return array_map('intval', explode(',', (string) $raw)); + $categoryIds = []; + foreach (explode(',', $raw) as $categoryId) { + $categoryId = trim($categoryId); + if (preg_match('/^-?\d+$/', $categoryId) === 1) { + $categoryIds[] = (int) $categoryId; + } + } + + return $categoryIds === [] ? [-1] : array_values(array_unique($categoryIds)); + } + + /** + * @param array|LengthAwarePaginator $results + */ + private function resolveSpellSuggestion(string $searchQuery, array|LengthAwarePaginator $results, bool $suggestEnabled): ?string + { + if ($searchQuery === '' || ! $suggestEnabled || $this->resultCount($results) > 3) { + return null; + } + + $suggestions = $this->searchService->suggest($searchQuery); + if ($suggestions === []) { + return null; + } + + usort($suggestions, static fn (array $a, array $b): int => (int) $b['docs'] <=> (int) $a['docs']); + + return $suggestions[0]['suggest'] !== $searchQuery ? $suggestions[0]['suggest'] : null; + } + + /** + * @param array|LengthAwarePaginator $results + */ + private function resultCount(array|LengthAwarePaginator $results): int + { + if ($results instanceof LengthAwarePaginator) { + return $results->total(); + } + + return count($results); + } + + /** + * @return array + */ + private function getCategorySelectOptions(): array + { + try { + return Cache::remember('search:category-select-options', now()->addMinutes(10), fn (): array => Category::getForSelect()); + } catch (\Throwable) { + return Category::getForSelect(); + } } } diff --git a/app/Http/Controllers/SeriesController.php b/app/Http/Controllers/SeriesController.php index 948921e66..bf8108fb9 100644 --- a/app/Http/Controllers/SeriesController.php +++ b/app/Http/Controllers/SeriesController.php @@ -37,17 +37,17 @@ class SeriesController extends BasePageController if ($id && ctype_digit($id)) { $category = -1; - if ($request->has('t') && ctype_digit($request->input('t'))) { - $category = $request->input('t'); + $categoryInput = $this->scalarInput($request, 't'); + if ($categoryInput !== '' && ctype_digit($categoryInput)) { + $category = (int) $categoryInput; } $catarray = []; $catarray[] = $category; $seriesLimit = (int) config('nntmux.series_view_limit', 200); - $page = $request->has('page') && is_numeric($request->input('page')) ? (int) $request->input('page') : 1; - $page = max($page, 1); - $offset = $seriesLimit > 0 ? ($page - 1) * $seriesLimit : 0; + $page = $this->resolvePage($request); + $offset = $seriesLimit > 0 ? $this->paginationOffset($page, $seriesLimit) : 0; $rel = $this->releaseSearchService->tvSearch(['id' => $id], '', '', '', $offset, $seriesLimit, '', $catarray, -1); @@ -175,7 +175,7 @@ class SeriesController extends BasePageController } else { $letter = ($id && preg_match('/^(0-9|[A-Z])$/i', $id)) ? $id : '0-9'; - $showname = ($request->has('title') && ! empty($request->input('title'))) ? $request->input('title') : ''; + $showname = $this->scalarInput($request, 'title'); if ($showname !== '' && ! $id) { $letter = ''; diff --git a/app/Models/UserMovie.php b/app/Models/UserMovie.php index 451dbf86a..6592bee63 100644 --- a/app/Models/UserMovie.php +++ b/app/Models/UserMovie.php @@ -41,7 +41,7 @@ class UserMovie extends Model protected $dateFormat = false; /** - * @param array $catID + * @param array $catID * @return int|Builder */ public static function addMovie(mixed $uid, mixed $imdbid, array $catID = []) // @phpstan-ignore missingType.generics @@ -97,7 +97,7 @@ class UserMovie extends Model } /** - * @param array $catID + * @param array $catID */ public static function updateMovie(mixed $uid, mixed $imdbid, array $catID = []): void { diff --git a/app/Models/UserSerie.php b/app/Models/UserSerie.php index 489ea4a8f..adebde5aa 100644 --- a/app/Models/UserSerie.php +++ b/app/Models/UserSerie.php @@ -54,7 +54,7 @@ class UserSerie extends Model * When a user wants to add a show to "my shows" insert it into the user series table. * * - * @param array $catID + * @param array $catID * @return int|Builder */ public static function addShow(mixed $userId, mixed $videoId, array $catID = []) // @phpstan-ignore missingType.generics @@ -125,7 +125,7 @@ class UserSerie extends Model /** * Update a TV show category ID for a user's "my show" TV show. * - * @param array $catID List of category ID's. + * @param array $catID List of category ID's. */ public static function updateShow(mixed $users_id, mixed $videos_id, array $catID = []): void { diff --git a/app/Services/Releases/ReleaseBrowseService.php b/app/Services/Releases/ReleaseBrowseService.php index 4b154af30..6c36b382d 100644 --- a/app/Services/Releases/ReleaseBrowseService.php +++ b/app/Services/Releases/ReleaseBrowseService.php @@ -164,25 +164,25 @@ class ReleaseBrowseService // Browse only needs columns used in browse/index.blade.php and search/index.blade.php $outerSelect = "SELECT r.id, r.searchname, r.guid, r.postdate, r.categories_id, r.size, r.totalpart, r.fromname, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus, r.group_name, CONCAT(cp.title, ' > ', c.title) AS category_name, - df.failed AS failed_count, - rr.total_report_count AS total_report_count, - rr.report_count AS report_count, - rr.report_reasons AS report_reasons, - rr.all_report_reasons AS all_report_reasons, - rr.latest_report_reason AS latest_report_reason, - rr.latest_report_description AS latest_report_description, - rr.response_count AS report_response_count, - rr.latest_response_at AS latest_report_response_at, + MAX(df.failed) AS failed_count, + COUNT(DISTINCT rr.id) AS total_report_count, + COUNT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.id ELSE NULL END) AS report_count, + GROUP_CONCAT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.reason ELSE NULL END SEPARATOR ', ') AS report_reasons, + GROUP_CONCAT(DISTINCT rr.reason SEPARATOR ', ') AS all_report_reasons, + SUBSTRING_INDEX(GROUP_CONCAT(rr.reason ORDER BY rr.created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, + SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(rr.description, ''), 'No additional report details were provided.') ORDER BY rr.created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, + COUNT(DISTINCT CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.id ELSE NULL END) AS report_response_count, + MAX(CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.responded_at ELSE NULL END) AS latest_report_response_at, rn.releases_id AS nfoid, re.releases_id AS reid, m.imdbid"; - $outerJoins = "LEFT JOIN categories c ON c.id = r.categories_id + $outerJoins = 'LEFT JOIN categories c ON c.id = r.categories_id LEFT JOIN root_categories cp ON cp.id = c.root_categories_id LEFT OUTER JOIN movieinfo m ON m.id = r.movieinfo_id LEFT OUTER JOIN video_data re ON re.releases_id = r.id LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id - LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS total_report_count, SUM(CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN 1 ELSE 0 END) AS report_count, GROUP_CONCAT(DISTINCT CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN reason ELSE NULL END SEPARATOR ', ') AS report_reasons, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS all_report_reasons, SUBSTRING_INDEX(GROUP_CONCAT(reason ORDER BY created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(description, ''), 'No additional report details were provided.') ORDER BY created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, SUM(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN 1 ELSE 0 END) AS response_count, MAX(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN responded_at ELSE NULL END) AS latest_response_at FROM release_reports GROUP BY releases_id) rr ON rr.releases_id = r.id"; + LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id'; $innerSelect = 'SELECT r.id, r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus, g.name AS group_name, r.movieinfo_id'; } diff --git a/app/Services/Releases/ReleaseSearchService.php b/app/Services/Releases/ReleaseSearchService.php index 9c5f9f79f..a5d40af00 100644 --- a/app/Services/Releases/ReleaseSearchService.php +++ b/app/Services/Releases/ReleaseSearchService.php @@ -1900,15 +1900,15 @@ class ReleaseSearchService r.totalpart, r.fromname, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus, CONCAT(cp.title, ' > ', c.title) AS category_name, - df.failed AS failed_count, - rr.total_report_count AS total_report_count, - rr.report_count AS report_count, - rr.report_reasons AS report_reasons, - rr.all_report_reasons AS all_report_reasons, - rr.latest_report_reason AS latest_report_reason, - rr.latest_report_description AS latest_report_description, - rr.response_count AS report_response_count, - rr.latest_response_at AS latest_report_response_at, + MAX(df.failed) AS failed_count, + COUNT(DISTINCT rr.id) AS total_report_count, + COUNT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.id ELSE NULL END) AS report_count, + GROUP_CONCAT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.reason ELSE NULL END SEPARATOR ', ') AS report_reasons, + GROUP_CONCAT(DISTINCT rr.reason SEPARATOR ', ') AS all_report_reasons, + SUBSTRING_INDEX(GROUP_CONCAT(rr.reason ORDER BY rr.created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, + SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(rr.description, ''), 'No additional report details were provided.') ORDER BY rr.created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, + COUNT(DISTINCT CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.id ELSE NULL END) AS report_response_count, + MAX(CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.responded_at ELSE NULL END) AS latest_report_response_at, g.name AS group_name, rn.releases_id AS nfoid, re.releases_id AS reid, @@ -1921,8 +1921,9 @@ class ReleaseSearchService LEFT JOIN categories c ON c.id = r.categories_id LEFT JOIN root_categories cp ON cp.id = c.root_categories_id LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id - LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS total_report_count, SUM(CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN 1 ELSE 0 END) AS report_count, GROUP_CONCAT(DISTINCT CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN reason ELSE NULL END SEPARATOR ', ') AS report_reasons, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS all_report_reasons, SUBSTRING_INDEX(GROUP_CONCAT(reason ORDER BY created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(description, ''), 'No additional report details were provided.') ORDER BY created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, SUM(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN 1 ELSE 0 END) AS response_count, MAX(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN responded_at ELSE NULL END) AS latest_response_at FROM release_reports GROUP BY releases_id) rr ON rr.releases_id = r.id - %s", + LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id + %s + GROUP BY r.id", $whereSql ); } @@ -2073,6 +2074,8 @@ class ReleaseSearchService // Remove ORDER BY clause (not needed for COUNT) $countQuery = preg_replace('/ORDER\s+BY\s+[^)]+$/is', '', $countQuery); + $hadGroupBy = preg_match('/GROUP\s+BY/is', $countQuery); + // Remove GROUP BY if it's only grouping by r.id $countQuery = preg_replace('/GROUP\s+BY\s+r\.id\s*$/is', '', $countQuery); @@ -2080,7 +2083,7 @@ class ReleaseSearchService $hasDistinct = preg_match('/SELECT\s+DISTINCT/is', $countQuery); // Replace SELECT clause with COUNT - if ($hasDistinct || preg_match('/GROUP\s+BY/is', $countQuery)) { + if ($hasDistinct || $hadGroupBy || preg_match('/GROUP\s+BY/is', $countQuery)) { // For queries with DISTINCT or GROUP BY, count distinct r.id $countQuery = preg_replace( '/SELECT\s+.+?\s+FROM/is', diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index 7baaffe6a..84e9d08a4 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -73,27 +73,16 @@ @if(count($results) > 0) -
-
-

- - {{ $catname ?? 'All' }} Books -

- -
-
- - - {{ $results->total() }} results found - -
-
+
@@ -173,4 +162,3 @@
@endsection - diff --git a/resources/views/browse/index.blade.php b/resources/views/browse/index.blade.php index 3b10781fd..d8e797988 100644 --- a/resources/views/browse/index.blade.php +++ b/resources/views/browse/index.blade.php @@ -24,317 +24,46 @@ @if($results->count() > 0) -
-
-
- -
- @if(isset($shows)) - - @endif + @php + $searchPlaceholder = 'Search'; + if (!empty($parentcat) && $parentcat !== 'All') { + $searchPlaceholder .= ' in ' . $parentcat; + if (!empty($catname) && $catname !== 'All' && $catname !== 'all') { + $searchPlaceholder .= ' ' . $catname; + } + } + $searchPlaceholder .= '...'; + @endphp - @if(isset($covgroup) && $covgroup != '' || isset($shows) && $shows) - - @endif - -
- With Selected: -
- - - @if(auth()->check() && auth()->user()->hasRole('Admin')) - - @endif -
-
+ + + @if(isset($shows)) + + @endif - -
-
- Showing {{ $results->firstItem() }} to {{ $results->lastItem() }} of {{ $results->total() }} results -
-
+ @if(isset($covgroup) && $covgroup != '' || isset($shows) && $shows) + + @endif +
- -
- @php - $searchPlaceholder = 'Search'; - if (!empty($parentcat) && $parentcat !== 'All') { - $searchPlaceholder .= ' in ' . $parentcat; - if (!empty($catname) && $catname !== 'All' && $catname !== 'all') { - $searchPlaceholder .= ' ' . $catname; - } - } - $searchPlaceholder .= '...'; - @endphp - - -
-
-
- - -
- {{ $results->links() }} -
- - - - - -
- @foreach($results as $result) -
-
- -
- @php - $mCoverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result); - $mHasCover = $mCoverUrl && !str_contains($mCoverUrl, 'no-cover.png'); - @endphp - @if($mHasCover) - query('thumbs') === '1') style="display:none" @endunless> - Cover - - @endif - - {{ $result->searchname }} - -
- @if(!empty($result->total_report_count) && $result->total_report_count > 0) - - Reported ({{ $result->total_report_count }}) - - @endif - @if(!empty($result->report_response_count) && $result->report_response_count > 0) - - Response - - @endif -
-
- - {{ $result->category_name ?? 'Other' }} - - {{ userDateDiffForHumans($result->adddate) }} - {{ $result->size_formatted ?? number_format($result->size / 1073741824, 2) . ' GB' }} - {{ $result->totalpart ?? 0 }} files - {{ $result->grabs ?? 0 }} -
- -
-
-
- @endforeach -
- - -
- {{ $results->links() }} -
- + + + + @else @endsection - diff --git a/resources/views/components/cover-release-list.blade.php b/resources/views/components/cover-release-list.blade.php new file mode 100644 index 000000000..85be3dcb3 --- /dev/null +++ b/resources/views/components/cover-release-list.blade.php @@ -0,0 +1,100 @@ +@props([ + 'releases', + 'totalReleases' => null, + 'max' => 2, + 'showCheckbox' => false, + 'showAddDate' => false, + 'showGroup' => false, + 'showStats' => false, +]) + +@php + $releaseItems = $releases instanceof \Illuminate\Support\Collection ? $releases->all() : (array) $releases; + $maxReleases = (int) $max; + $displayReleases = array_slice($releaseItems, 0, $maxReleases); + $totalReleaseCount = $totalReleases ?? count($releaseItems); + $shouldShowCheckbox = filter_var($showCheckbox, FILTER_VALIDATE_BOOL); + $shouldShowAddDate = filter_var($showAddDate, FILTER_VALIDATE_BOOL); + $shouldShowGroup = filter_var($showGroup, FILTER_VALIDATE_BOOL); + $shouldShowStats = filter_var($showStats, FILTER_VALIDATE_BOOL); +@endphp + +@if(!empty($displayReleases)) +
+

+ Available Releases + @if($totalReleaseCount > $maxReleases) + (Showing {{ $maxReleases }} of {{ $totalReleaseCount }}) + @endif +

+
+ @foreach($displayReleases as $release) + @if(!empty($release->searchname)) +
+
+
+ + {{ $release->searchname }} + + @if($shouldShowCheckbox) + + @endif +
+ +
+ @if(isset($release->size)) + + {{ number_format($release->size / 1073741824, 2) }} GB + + @endif + @if(isset($release->postdate)) + + {{ date('M d, Y H:i', strtotime($release->postdate)) }} + + @endif + @if($shouldShowAddDate && isset($release->adddate)) + + {{ userDateDiffForHumans($release->adddate) }} + + @endif + @if((isset($release->nfoid) && !empty($release->nfoid)) || (isset($release->nfostatus) && (int) $release->nfostatus === 1)) + + @endif + @if($shouldShowGroup && isset($release->group_name) && !empty($release->group_name)) + + {{ $release->group_name }} + + @endif +
+ + +
+
+ @endif + @endforeach +
+
+@endif diff --git a/resources/views/components/cover-results-toolbar.blade.php b/resources/views/components/cover-results-toolbar.blade.php new file mode 100644 index 000000000..676d37dc0 --- /dev/null +++ b/resources/views/components/cover-results-toolbar.blade.php @@ -0,0 +1,37 @@ +@props([ + 'results', + 'icon', + 'title', + 'covgroup', + 'category' => 'All', + 'parentcat', + 'searchPlaceholder', + 'searchCategory' => null, + 'totalLabel' => 'results found', +]) + +@php + $totalResults = is_object($results) && method_exists($results, 'total') ? $results->total() : count($results); +@endphp + +
+
+

+ + {{ $title }} +

+ +
+
+ + + {{ $totalResults }} {{ $totalLabel }} + +
+
diff --git a/resources/views/components/release-results-panel.blade.php b/resources/views/components/release-results-panel.blade.php new file mode 100644 index 000000000..ebecd1a60 --- /dev/null +++ b/resources/views/components/release-results-panel.blade.php @@ -0,0 +1,81 @@ +@props([ + 'results', + 'showThumbs' => false, + 'dateField' => 'adddate', + 'showTopPagination' => false, + 'showBottomPagination' => true, + 'showSort' => true, +]) + +@php + $hasPaginatorLinks = is_object($results) && method_exists($results, 'links'); + $shouldShowThumbs = filter_var($showThumbs, FILTER_VALIDATE_BOOL); +@endphp + +
+
+
+
+ @isset($beforeActions) + {{ $beforeActions }} + @endisset + +
+ With Selected: +
+ + + @if(auth()->check() && auth()->user()->hasRole('Admin')) + + @endif +
+
+
+ +
+
+ @isset($summary) + {{ $summary }} + @else + Showing {{ $results->firstItem() }} to {{ $results->lastItem() }} of {{ $results->total() }} results + @endisset +
+
+ +
+ @isset($toolbarRight) + {{ $toolbarRight }} + @endisset + + @if($showSort) + + @endif +
+
+
+ + @if($showTopPagination && $hasPaginatorLinks) +
+ {{ $results->links() }} +
+ @endif + + + + @if($showBottomPagination && $hasPaginatorLinks) +
+ {{ $results->appends(request()->query())->links() }} +
+ @endif + diff --git a/resources/views/components/release-results.blade.php b/resources/views/components/release-results.blade.php new file mode 100644 index 000000000..df70d4900 --- /dev/null +++ b/resources/views/components/release-results.blade.php @@ -0,0 +1,271 @@ +@props([ + 'results', + 'showThumbs' => false, + 'dateField' => 'adddate', +]) + +@php + $shouldShowThumbs = filter_var($showThumbs, FILTER_VALIDATE_BOOL); + $activeDateField = (string) $dateField; +@endphp + + + + + +
+ @foreach($results as $result) + @php + $reportedCount = (int) ($result->total_report_count ?? $result->report_count ?? 0); + $responseCount = (int) ($result->report_response_count ?? 0); + $sizeLabel = $result->size_formatted ?? number_format(($result->size ?? 0) / 1073741824, 2) . ' GB'; + $dateValue = $result->{$activeDateField} ?? $result->adddate ?? $result->postdate ?? null; + @endphp +
+
+ +
+ @if($shouldShowThumbs) + @php + $mCoverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result); + $mHasCover = $mCoverUrl && !str_contains($mCoverUrl, 'no-cover.png'); + @endphp + @if($mHasCover) + query('thumbs') === '1') style="display:none" @endunless> + Cover + + @endif + @endif + + {{ $result->searchname }} + +
+ @if($reportedCount > 0) + + Reported ({{ $reportedCount }}) + + @endif + @if($responseCount > 0) + + Response + + @endif + @if(!empty($result->failed_count) && $result->failed_count > 0) + + Failed ({{ $result->failed_count }}) + + @endif +
+
+ + {{ $result->category_name ?? 'Other' }} + + {{ $dateValue ? userDateDiffForHumans($dateValue) : 'Unknown' }} + {{ $sizeLabel }} + {{ $result->totalpart ?? 0 }} files + {{ $result->grabs ?? 0 }} + {{ $result->comments ?? 0 }} +
+
+ + + + + + + + + + @if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid)) + + + + @endif + +
+
+
+
+ @endforeach +
diff --git a/resources/views/console/index.blade.php b/resources/views/console/index.blade.php index d5d811dde..07eba2e6a 100644 --- a/resources/views/console/index.blade.php +++ b/resources/views/console/index.blade.php @@ -62,9 +62,7 @@ @php $releases = $result->releases ?? []; $totalReleases = $result->total_releases ?? count($releases); - $maxReleases = 2; - $displayReleases = array_slice($releases, 0, $maxReleases); - $guid = !empty($displayReleases) ? $displayReleases[0]->guid : null; + $guid = !empty($releases) ? $releases[0]->guid : null; $totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0)); @endphp @@ -137,87 +135,14 @@
- - @if(!empty($displayReleases)) -
-

- Available Releases - @if($totalReleases > $maxReleases) - (Showing {{ $maxReleases }} of {{ $totalReleases }}) - @endif -

-
- @foreach($displayReleases as $release) - @if($release->searchname) -
-
- - - -
- @if(isset($release->size)) - - {{ number_format($release->size / 1073741824, 2) }} GB - - @endif - @if(isset($release->postdate)) - - {{ date('M d, Y H:i', strtotime($release->postdate)) }} - - @endif - @if(isset($release->adddate)) - - {{ userDateDiffForHumans($release->adddate) }} - - @endif - @if(isset($release->nfoid) && !empty($release->nfoid)) - - @endif - @if(isset($release->group_name) && !empty($release->group_name)) - - {{ $release->group_name }} - - @endif -
- - - -
-
- @endif - @endforeach -
-
- @endif + diff --git a/resources/views/details/index.blade.php b/resources/views/details/index.blade.php index e72bc7c9b..f871125a2 100644 --- a/resources/views/details/index.blade.php +++ b/resources/views/details/index.blade.php @@ -61,6 +61,11 @@ View NFO @endif + @if(($release->totalpart ?? 0) > 0) + + @endif @auth @if(auth()->user()->hasRole('Admin') || auth()->user()->hasRole('Moderator')) @@ -947,41 +952,6 @@ @endif - - @if($nfo ?? false) -
-

NFO

-
-
{{ $nfo }}
-
-
- @endif - - - @if(isset($files) && count($files) > 0) -
-

Files ({{ count($files) }})

-
- - - - - - - - - @foreach($files as $file) - - - - - @endforeach - -
FilenameSize
{{ $file->name }}{{ number_format($file->size / 1048576, 2) }} MB
-
-
- @endif -

@@ -1156,4 +1126,3 @@ @push('scripts') @include('partials.cart-script') @endpush - diff --git a/resources/views/games/index.blade.php b/resources/views/games/index.blade.php index 6990da32e..761b8066c 100644 --- a/resources/views/games/index.blade.php +++ b/resources/views/games/index.blade.php @@ -96,27 +96,16 @@ @if(count($results) > 0) -
-
-

- - {{ $catname ?? 'All' }} Games -

- -
-
- - - {{ $results->total() }} results found - -
-
+
@@ -124,9 +113,7 @@ @php $releases = $result->releases ?? []; $totalReleases = $result->total_releases ?? count($releases); - $maxReleases = 2; - $displayReleases = array_slice($releases, 0, $maxReleases); - $guid = !empty($displayReleases) ? $displayReleases[0]->guid : null; + $guid = !empty($releases) ? $releases[0]->guid : null; $totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0)); @endphp @@ -189,66 +176,7 @@

- - @if(!empty($displayReleases)) -
-

- Available Releases - @if($totalReleases > $maxReleases) - (Showing {{ $maxReleases }} of {{ $totalReleases }}) - @endif -

-
- @foreach($displayReleases as $release) - @if($release->searchname) -
-
- - - {{ $release->searchname }} - - - -
- @if(isset($release->size)) - - {{ number_format($release->size / 1073741824, 2) }} GB - - @endif - @if(isset($release->postdate)) - - {{ date('M d, Y H:i', strtotime($release->postdate)) }} - - @endif - @if(isset($release->nfoid) && !empty($release->nfoid)) - - @endif -
- - - -
-
- @endif - @endforeach -
-
- @endif + @@ -275,4 +203,3 @@ {{-- NFO modal is included globally via layouts.main --}} @endsection - diff --git a/resources/views/music/index.blade.php b/resources/views/music/index.blade.php index 255646e7e..f0f145ba3 100644 --- a/resources/views/music/index.blade.php +++ b/resources/views/music/index.blade.php @@ -93,27 +93,16 @@ @if(count($results) > 0) -
-
-

- - {{ $catname ?? 'All' }} Albums -

- -
-
- - - {{ $results->total() }} results found - -
-
+
@@ -121,9 +110,7 @@ @php $releases = $result->releases ?? []; $totalReleases = $result->total_releases ?? count($releases); - $maxReleases = 2; - $displayReleases = array_slice($releases, 0, $maxReleases); - $guid = !empty($displayReleases) ? $displayReleases[0]->guid : null; + $guid = !empty($releases) ? $releases[0]->guid : null; $totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0)); @endphp @@ -182,66 +169,7 @@
- - @if(!empty($displayReleases)) -
-

- Available Releases - @if($totalReleases > $maxReleases) - (Showing {{ $maxReleases }} of {{ $totalReleases }}) - @endif -

-
- @foreach($displayReleases as $release) - @if($release->searchname) -
-
- - - {{ $release->searchname }} - - - -
- @if(isset($release->size)) - - {{ number_format($release->size / 1073741824, 2) }} GB - - @endif - @if(isset($release->postdate)) - - {{ date('M d, Y H:i', strtotime($release->postdate)) }} - - @endif - @if(isset($release->nfoid) && !empty($release->nfoid)) - - @endif -
- - - -
-
- @endif - @endforeach -
-
- @endif + @@ -268,4 +196,3 @@ {{-- NFO modal is included globally via layouts.main --}} @endsection - diff --git a/resources/views/search/index.blade.php b/resources/views/search/index.blade.php index 2a56ec060..5a9c01e15 100644 --- a/resources/views/search/index.blade.php +++ b/resources/views/search/index.blade.php @@ -158,246 +158,14 @@ @if(isset($results) && ((is_array($results) && count($results) > 0) || (is_object($results) && $results->count() > 0))) -
- -
-
-
- With Selected: -
- - - @if(auth()->check() && auth()->user()->hasRole('Admin')) - - @endif -
-
-
-
- {{ is_object($results) ? $results->total() : count($results) }} results found - @if(is_object($results)) - — Page {{ $results->currentPage() }} of {{ $results->lastPage() }} - @endif -
-
-
- -
-
-
- - - - - -
- @foreach($results as $result) -
-
- -
- - {{ $result->searchname }} - -
- - {{ $result->category_name ?? 'Other' }} - - {{ userDateDiffForHumans($result->postdate) }} - {{ number_format($result->size / 1073741824, 2) }} GB - {{ $result->totalpart ?? 0 }} files - {{ $result->grabs ?? 0 }} - {{ $result->comments ?? 0 }} -
-
- - - - - - - - - - @if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid)) - - - - @endif - -
-
-
-
- @endforeach -
- - - @if(is_object($results) && method_exists($results, 'links')) -
- {{ $results->appends(request()->query())->links() }} -
- @endif -
+ + + {{ is_object($results) ? $results->total() : count($results) }} results found + @if(is_object($results)) + - Page {{ $results->currentPage() }} of {{ $results->lastPage() }} + @endif + + @elseif(request()->has('search')) @endsection - diff --git a/tests/Feature/AdminContentControllerTest.php b/tests/Feature/AdminContentControllerTest.php index 897fa5308..b36e47d40 100644 --- a/tests/Feature/AdminContentControllerTest.php +++ b/tests/Feature/AdminContentControllerTest.php @@ -141,6 +141,29 @@ class AdminContentControllerTest extends TestCase $this->assertSame('/about/', $content->url); } + public function test_content_page_accepts_string_content_id_from_query(): void + { + $user = $this->createUserWithRole('User'); + /** @var Authenticatable $authenticatedUser */ + $authenticatedUser = $user; + + $content = $this->createContent([ + 'title' => 'Useful Content', + 'body' => '

Visible body.

', + 'contenttype' => Content::TYPE_USEFUL, + 'status' => Content::STATUS_ENABLED, + 'role' => Content::ROLE_LOGGED_IN, + ]); + + $response = $this->actingAs($authenticatedUser)->get(route('content', [ + 'id' => (string) $content->id, + 'page' => 'content', + ])); + + $response->assertOk(); + $response->assertSee('Useful Content'); + } + public function test_create_form_marks_title_as_optional(): void { $admin = $this->createUserWithRole('Admin'); diff --git a/tests/Feature/AdminReleaseReportControllerTest.php b/tests/Feature/AdminReleaseReportControllerTest.php index a669e1395..63a3e45b0 100644 --- a/tests/Feature/AdminReleaseReportControllerTest.php +++ b/tests/Feature/AdminReleaseReportControllerTest.php @@ -306,17 +306,17 @@ class AdminReleaseReportControllerTest extends TestCase { $detailsControllerPath = app_path('Http/Controllers/DetailsController.php'); $detailsViewPath = resource_path('views/details/index.blade.php'); - $browseViewPath = resource_path('views/browse/index.blade.php'); + $releaseResultsComponentPath = resource_path('views/components/release-results.blade.php'); $browseServicePath = app_path('Services/Releases/ReleaseBrowseService.php'); $this->assertFileExists($detailsControllerPath); $this->assertFileExists($detailsViewPath); - $this->assertFileExists($browseViewPath); + $this->assertFileExists($releaseResultsComponentPath); $this->assertFileExists($browseServicePath); $detailsController = file_get_contents($detailsControllerPath); $detailsView = file_get_contents($detailsViewPath); - $browseView = file_get_contents($browseViewPath); + $releaseResultsComponent = file_get_contents($releaseResultsComponentPath); $browseService = file_get_contents($browseServicePath); $this->assertStringContainsString('publicReportResponses', $detailsController); @@ -330,7 +330,7 @@ class AdminReleaseReportControllerTest extends TestCase $this->assertStringContainsString('all_report_reasons', $browseService); $this->assertStringContainsString('report_response_count', $browseService); $this->assertStringContainsString('response_is_public = 1', $browseService); - $this->assertStringContainsString('Original report:', $browseView); - $this->assertStringContainsString('Staff response available on release details', $browseView); + $this->assertStringContainsString('Original report:', $releaseResultsComponent); + $this->assertStringContainsString('Staff response available on release details', $releaseResultsComponent); } } diff --git a/tests/Unit/CoverBrowseComponentsTest.php b/tests/Unit/CoverBrowseComponentsTest.php new file mode 100644 index 000000000..6658e3489 --- /dev/null +++ b/tests/Unit/CoverBrowseComponentsTest.php @@ -0,0 +1,39 @@ +assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringNotContainsString('$displayReleases = array_slice', $console); + $this->assertStringNotContainsString('$displayReleases = array_slice', $music); + $this->assertStringNotContainsString('$displayReleases = array_slice', $games); + + $this->assertStringContainsString('nfo-badge', $component); + $this->assertStringContainsString('add-to-cart', $component); + $this->assertStringContainsString('chkRelease', $component); + $this->assertStringContainsString('Available Releases', $component); + } +} diff --git a/tests/Unit/ReleaseBrowseServiceTest.php b/tests/Unit/ReleaseBrowseServiceTest.php index 81898a116..a934cbb6e 100644 --- a/tests/Unit/ReleaseBrowseServiceTest.php +++ b/tests/Unit/ReleaseBrowseServiceTest.php @@ -148,4 +148,17 @@ class ReleaseBrowseServiceTest extends TestCase $this->assertEquals(['searchname', 'desc'], $result); } + + /** + * Test that browse result report badges use joined page rows, not a full-table report aggregate. + */ + public function test_browse_query_uses_page_scoped_report_join(): void + { + $servicePath = __DIR__.'/../../app/Services/Releases/ReleaseBrowseService.php'; + $content = (string) file_get_contents($servicePath); + + $this->assertStringContainsString('LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id', $content); + $this->assertStringContainsString('COUNT(DISTINCT rr.id) AS total_report_count', $content); + $this->assertStringNotContainsString('FROM release_reports GROUP BY releases_id', $content); + } } diff --git a/tests/Unit/ReleaseResultsComponentTest.php b/tests/Unit/ReleaseResultsComponentTest.php new file mode 100644 index 000000000..bfd5ef105 --- /dev/null +++ b/tests/Unit/ReleaseResultsComponentTest.php @@ -0,0 +1,47 @@ +assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringNotContainsString('@foreach($results as $result)', $browse); + $this->assertStringNotContainsString('@foreach($results as $result)', $search); + } + + public function test_shared_release_results_panel_keeps_bulk_actions_and_toolbar_slots(): void + { + $panel = (string) file_get_contents(__DIR__.'/../../resources/views/components/release-results-panel.blade.php'); + + $this->assertStringContainsString('nzb_multi_operations_form', $panel); + $this->assertStringContainsString('nzb_multi_operations_download', $panel); + $this->assertStringContainsString('nzb_multi_operations_cart', $panel); + $this->assertStringContainsString('nzb_multi_operations_delete', $panel); + $this->assertStringContainsString('$beforeActions', $panel); + $this->assertStringContainsString('$toolbarRight', $panel); + $this->assertStringContainsString('$summary', $panel); + } + + public function test_shared_release_results_component_keeps_expected_release_actions(): void + { + $component = (string) file_get_contents(__DIR__.'/../../resources/views/components/release-results.blade.php'); + + $this->assertStringContainsString('download-nzb', $component); + $this->assertStringContainsString('add-to-cart', $component); + $this->assertStringContainsString('filelist-badge', $component); + $this->assertStringContainsString('nfo-badge', $component); + $this->assertStringContainsString('preview-badge', $component); + $this->assertStringContainsString('mediainfo-badge', $component); + $this->assertStringContainsString('assertStringNotContainsString('JOIN', $sql); } + /** + * Test that web search report badge data is aggregated on joined result rows. + */ + public function test_build_search_base_sql_uses_page_scoped_report_join(): void + { + $reflection = new ReflectionClass(ReleaseSearchService::class); + $method = $reflection->getMethod('buildSearchBaseSql'); + + $sql = $method->invoke($this->service, 'WHERE r.id IN (1,2,3)'); + + $this->assertStringContainsString('LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id', $sql); + $this->assertStringContainsString('COUNT(DISTINCT rr.id) AS total_report_count', $sql); + $this->assertStringContainsString('GROUP BY r.id', $sql); + $this->assertStringNotContainsString('FROM release_reports GROUP BY releases_id', $sql); + } + /** * Test that the lightweight count query is built directly against releases. */ diff --git a/tests/Unit/UserViewRequestInputTest.php b/tests/Unit/UserViewRequestInputTest.php new file mode 100644 index 000000000..256a57fae --- /dev/null +++ b/tests/Unit/UserViewRequestInputTest.php @@ -0,0 +1,101 @@ +controller(); + + $this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => ['2']]))); + $this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => -4]))); + $this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => '3.5']))); + $this->assertSame(3, $controller->page(Request::create('/browse', 'GET', ['page' => '3']))); + } + + public function test_order_resolution_only_allows_known_sort_keys(): void + { + $controller = $this->controller(); + $ordering = ['name_asc', 'postdate_desc']; + + $this->assertSame('name_asc', $controller->order(Request::create('/browse', 'GET', ['ob' => 'name_asc']), $ordering)); + $this->assertSame('', $controller->order(Request::create('/browse', 'GET', ['ob' => 'name_desc']), $ordering)); + $this->assertSame('', $controller->order(Request::create('/browse', 'GET', ['ob' => ['name_asc']]), $ordering)); + } + + public function test_scalar_input_and_offset_helpers_normalize_user_query_values(): void + { + $controller = $this->controller(); + + $this->assertSame('', $controller->scalar(Request::create('/search', 'GET', ['title' => ['bad']]), 'title')); + $this->assertSame('ubuntu', $controller->scalar(Request::create('/search', 'GET', ['title' => 'ubuntu']), 'title')); + $this->assertSame(42, $controller->integer(Request::create('/browse', 'GET', ['t' => '42']), 't', 100)); + $this->assertSame(100, $controller->integer(Request::create('/browse', 'GET', ['t' => '42.2']), 't', 100)); + $this->assertSame(100, $controller->integer(Request::create('/browse', 'GET', ['t' => ['42']]), 't', 100)); + $this->assertSame(['7010', '7020'], $controller->array(Request::create('/browse', 'GET', ['category' => ['7010', '7020']]), 'category')); + $this->assertSame([], $controller->array(Request::create('/browse', 'GET', ['category' => '7010']), 'category')); + $this->assertSame(80, $controller->offset(5, 20)); + } + + public function test_search_category_resolution_rejects_malformed_category_values(): void + { + $reflection = new ReflectionClass(SearchController::class); + $controller = $reflection->newInstanceWithoutConstructor(); + $method = $reflection->getMethod('resolveCategoryIdsFromRequest'); + + $this->assertSame([1000, 2000], $method->invoke($controller, Request::create('/search', 'GET', ['t' => '1000,bad,2000,1000']))); + $this->assertSame([-1], $method->invoke($controller, Request::create('/search', 'GET', ['t' => ['1000']]))); + $this->assertSame([-1], $method->invoke($controller, Request::create('/search', 'GET', ['searchadvcat' => '']))); + } + + private function controller(): object + { + return new class extends BasePageController + { + public function __construct() {} + + public function page(Request $request): int + { + return $this->resolvePage($request); + } + + /** + * @param array $ordering + */ + public function order(Request $request, array $ordering): string + { + return $this->resolveOrderBy($request, $ordering); + } + + public function scalar(Request $request, string $key): string + { + return $this->scalarInput($request, $key); + } + + public function integer(Request $request, string $key, int $default): int + { + return $this->integerInput($request, $key, $default); + } + + /** + * @return array + */ + public function array(Request $request, string $key): array + { + return $this->arrayInput($request, $key); + } + + public function offset(int $page, int $perPage): int + { + return $this->paginationOffset($page, $perPage); + } + }; + } +}