Total Complexity | 94 |
Total Lines | 471 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 1 | Features | 0 |
Complex classes like ApiController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ApiController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
25 | class ApiController extends BasePageController |
||
26 | { |
||
27 | private string $type; |
||
28 | |||
29 | /** |
||
30 | * @return Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector|StreamedResponse|void |
||
31 | * |
||
32 | * @throws \Throwable |
||
33 | */ |
||
34 | public function api(Request $request) |
||
35 | { |
||
36 | // API functions. |
||
37 | $function = 's'; |
||
38 | if ($request->has('t')) { |
||
39 | switch ($request->input('t')) { |
||
40 | case 'd': |
||
41 | case 'details': |
||
42 | $function = 'd'; |
||
43 | break; |
||
44 | case 'g': |
||
45 | case 'get': |
||
46 | $function = 'g'; |
||
47 | break; |
||
48 | case 's': |
||
49 | case 'search': |
||
50 | break; |
||
51 | case 'c': |
||
52 | case 'caps': |
||
53 | $function = 'c'; |
||
54 | break; |
||
55 | case 'tv': |
||
56 | case 'tvsearch': |
||
57 | $function = 'tv'; |
||
58 | break; |
||
59 | case 'm': |
||
60 | case 'movie': |
||
61 | $function = 'm'; |
||
62 | break; |
||
63 | case 'gn': |
||
64 | case 'n': |
||
65 | case 'nfo': |
||
66 | case 'info': |
||
67 | $function = 'n'; |
||
68 | break; |
||
69 | default: |
||
70 | return Utility::showApiError(202, 'No such function ('.$request->input('t').')'); |
||
71 | } |
||
72 | } else { |
||
73 | return Utility::showApiError(200, 'Missing parameter (t)'); |
||
74 | } |
||
75 | |||
76 | $uid = $apiKey = $oldestGrabTime = $thisOldestTime = ''; |
||
77 | $res = $catExclusions = []; |
||
78 | $maxRequests = $thisRequests = $maxDownloads = $grabs = 0; |
||
79 | |||
80 | // Page is accessible only by the apikey |
||
81 | |||
82 | if ($function !== 'c' && $function !== 'r') { |
||
83 | if ($request->missing('apikey') || ($request->has('apikey') && empty($request->input('apikey')))) { |
||
84 | return Utility::showApiError(200, 'Missing parameter (apikey)'); |
||
85 | } else { |
||
86 | $apiKey = $request->input('apikey'); |
||
87 | $res = User::getByRssToken($apiKey); |
||
88 | if ($res === null) { |
||
89 | return Utility::showApiError(100, 'Incorrect user credentials (wrong API key)'); |
||
90 | } |
||
91 | } |
||
92 | |||
93 | if ($res->hasRole('Disabled')) { |
||
94 | return Utility::showApiError(101); |
||
95 | } |
||
96 | |||
97 | $uid = $res->id; |
||
98 | $catExclusions = User::getCategoryExclusionForApi($request); |
||
99 | $maxRequests = $res->role->apirequests; |
||
|
|||
100 | $maxDownloads = $res->role->downloadrequests; |
||
101 | $time = UserRequest::whereUsersId($uid)->min('timestamp'); |
||
102 | $thisOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; |
||
103 | $grabTime = UserDownload::whereUsersId($uid)->min('timestamp'); |
||
104 | $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; |
||
105 | } |
||
106 | |||
107 | // Record user access to the api, if its been called by a user (i.e. capabilities request do not require a user to be logged in or key provided). |
||
108 | if ($uid !== '') { |
||
109 | event(new UserAccessedApi($res)); |
||
110 | $thisRequests = UserRequest::getApiRequests($uid); |
||
111 | $grabs = UserDownload::getDownloadRequests($uid); |
||
112 | if ($thisRequests > $maxRequests) { |
||
113 | return Utility::showApiError(500, 'Request limit reached ('.$thisRequests.'/'.$maxRequests.')'); |
||
114 | } |
||
115 | } |
||
116 | |||
117 | $releases = new Releases; |
||
118 | |||
119 | // Set Query Parameters based on Request objects |
||
120 | $outputXML = ! ($request->has('o') && $request->input('o') === 'json'); |
||
121 | $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; |
||
122 | $offset = $this->offset($request); |
||
123 | |||
124 | // Set API Parameters based on Request objects |
||
125 | $params['extended'] = $request->has('extended') && (int) $request->input('extended') === 1 ? '1' : '0'; |
||
126 | $params['del'] = $request->has('del') && (int) $request->input('del') === 1 ? '1' : '0'; |
||
127 | $params['uid'] = $uid; |
||
128 | $params['token'] = $apiKey; |
||
129 | $params['apilimit'] = $maxRequests; |
||
130 | $params['requests'] = $thisRequests; |
||
131 | $params['downloadlimit'] = $maxDownloads; |
||
132 | $params['grabs'] = $grabs; |
||
133 | $params['oldestapi'] = $thisOldestTime; |
||
134 | $params['oldestgrab'] = $oldestGrabTime; |
||
135 | |||
136 | switch ($function) { |
||
137 | // Search releases. |
||
138 | case 's': |
||
139 | $this->verifyEmptyParameter($request, 'q'); |
||
140 | $maxAge = $this->maxAge($request); |
||
141 | $groupName = $this->group($request); |
||
142 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
143 | $categoryID = $this->categoryID($request); |
||
144 | $limit = $this->limit($request); |
||
145 | $searchArr = [ |
||
146 | 'searchname' => $request->input('q') ?? -1, |
||
147 | 'name' => -1, |
||
148 | 'fromname' => -1, |
||
149 | 'filename' => -1, |
||
150 | ]; |
||
151 | |||
152 | if ($request->has('q')) { |
||
153 | $relData = $releases->search( |
||
154 | $searchArr, |
||
155 | $groupName, |
||
156 | -1, |
||
157 | -1, |
||
158 | -1, |
||
159 | -1, |
||
160 | $offset, |
||
161 | $limit, |
||
162 | '', |
||
163 | $maxAge, |
||
164 | $catExclusions, |
||
165 | 'basic', |
||
166 | $categoryID, |
||
167 | $minSize |
||
168 | ); |
||
169 | } else { |
||
170 | $relData = $releases->getBrowseRange( |
||
171 | 1, |
||
172 | $categoryID, |
||
173 | $offset, |
||
174 | $limit, |
||
175 | '', |
||
176 | $maxAge, |
||
177 | $catExclusions, |
||
178 | $groupName, |
||
179 | $minSize |
||
180 | ); |
||
181 | } |
||
182 | $this->output($relData, $params, $outputXML, $offset, 'api'); |
||
183 | break; |
||
184 | // Search tv releases. |
||
185 | case 'tv': |
||
186 | $this->verifyEmptyParameter($request, 'q'); |
||
187 | $this->verifyEmptyParameter($request, 'vid'); |
||
188 | $this->verifyEmptyParameter($request, 'tvdbid'); |
||
189 | $this->verifyEmptyParameter($request, 'traktid'); |
||
190 | $this->verifyEmptyParameter($request, 'rid'); |
||
191 | $this->verifyEmptyParameter($request, 'tvmazeid'); |
||
192 | $this->verifyEmptyParameter($request, 'imdbid'); |
||
193 | $this->verifyEmptyParameter($request, 'tmdbid'); |
||
194 | $this->verifyEmptyParameter($request, 'season'); |
||
195 | $this->verifyEmptyParameter($request, 'ep'); |
||
196 | $maxAge = $this->maxAge($request); |
||
197 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
198 | |||
199 | $siteIdArr = [ |
||
200 | 'id' => $request->input('vid') ?? '0', |
||
201 | 'tvdb' => $request->input('tvdbid') ?? '0', |
||
202 | 'trakt' => $request->input('traktid') ?? '0', |
||
203 | 'tvrage' => $request->input('rid') ?? '0', |
||
204 | 'tvmaze' => $request->input('tvmazeid') ?? '0', |
||
205 | 'imdb' => Str::replace('tt', '', $request->input('imdbid')) ?? '0', |
||
206 | 'tmdb' => $request->input('tmdbid') ?? '0', |
||
207 | ]; |
||
208 | |||
209 | // Process season only queries or Season and Episode/Airdate queries |
||
210 | |||
211 | $series = $request->input('season') ?? ''; |
||
212 | $episode = $request->input('ep') ?? ''; |
||
213 | |||
214 | if (preg_match('#^(19|20)\d{2}$#', $series, $year) && str_contains($episode, '/')) { |
||
215 | $airDate = str_replace('/', '-', $year[0].'-'.$episode); |
||
216 | } |
||
217 | |||
218 | $relData = $releases->tvSearch( |
||
219 | $siteIdArr, |
||
220 | $series, |
||
221 | $episode, |
||
222 | $airDate ?? '', |
||
223 | $this->offset($request), |
||
224 | $this->limit($request), |
||
225 | $request->input('q') ?? '', |
||
226 | $this->categoryID($request), |
||
227 | $maxAge, |
||
228 | $minSize, |
||
229 | $catExclusions |
||
230 | ); |
||
231 | |||
232 | $this->output($relData, $params, $outputXML, $offset, 'api'); |
||
233 | break; |
||
234 | |||
235 | // Search movie releases. |
||
236 | case 'm': |
||
237 | $this->verifyEmptyParameter($request, 'q'); |
||
238 | $this->verifyEmptyParameter($request, 'imdbid'); |
||
239 | $maxAge = $this->maxAge($request); |
||
240 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
241 | |||
242 | $imdbId = $request->has('imdbid') && $request->filled('imdbid') ? (int) $request->input('imdbid') : -1; |
||
243 | $tmdbId = $request->has('tmdbid') && $request->filled('tmdbid') ? (int) $request->input('tmdbid') : -1; |
||
244 | $traktId = $request->has('traktid') && $request->filled('traktid') ? (int) $request->input('traktid') : -1; |
||
245 | |||
246 | $relData = $releases->moviesSearch( |
||
247 | $imdbId, |
||
248 | $tmdbId, |
||
249 | $traktId, |
||
250 | $this->offset($request), |
||
251 | $this->limit($request), |
||
252 | $request->input('q') ?? '', |
||
253 | $this->categoryID($request), |
||
254 | $maxAge, |
||
255 | $minSize, |
||
256 | $catExclusions |
||
257 | ); |
||
258 | |||
259 | $this->addCoverURL( |
||
260 | $relData, |
||
261 | function ($release) { |
||
262 | return Utility::getCoverURL(['type' => 'movies', 'id' => $release->imdbid]); |
||
263 | } |
||
264 | ); |
||
265 | |||
266 | $this->output($relData, $params, $outputXML, $offset, 'api'); |
||
267 | break; |
||
268 | |||
269 | // Get NZB. |
||
270 | case 'g': |
||
271 | $this->verifyEmptyParameter($request, 'g'); |
||
272 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
273 | $relData = Release::checkGuidForApi($request->input('id')); |
||
274 | if ($relData) { |
||
275 | return redirect(url('/getnzb?r='.$apiKey.'&id='.$request->input('id').(($request->has('del') && $request->input('del') === '1') ? '&del=1' : ''))); |
||
276 | } |
||
277 | |||
278 | return Utility::showApiError(300, 'No such item (the guid you provided has no release in our database)'); |
||
279 | |||
280 | // Get individual NZB details. |
||
281 | case 'd': |
||
282 | if ($request->missing('id')) { |
||
283 | return Utility::showApiError(200, 'Missing parameter (guid is required for single release details)'); |
||
284 | } |
||
285 | |||
286 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
287 | $data = Release::getByGuid($request->input('id')); |
||
288 | |||
289 | $this->output($data, $params, $outputXML, $offset, 'api'); |
||
290 | break; |
||
291 | |||
292 | // Get an NFO file for an individual release. |
||
293 | case 'n': |
||
294 | if ($request->missing('id')) { |
||
295 | return Utility::showApiError(200, 'Missing parameter (id is required for retrieving an NFO)'); |
||
296 | } |
||
297 | |||
298 | UserRequest::addApiRequest($apiKey, $request->getRequestUri()); |
||
299 | $rel = Release::query()->where('guid', $request->input('id'))->first(['id', 'searchname']); |
||
300 | |||
301 | if ($rel && $rel->isNotEmpty()) { |
||
302 | $data = ReleaseNfo::getReleaseNfo($rel->id); |
||
303 | if (! empty($data)) { |
||
304 | if ($request->has('o') && $request->input('o') === 'file') { |
||
305 | return response()->streamDownload(function () use ($data) { |
||
306 | echo $data['nfo']; |
||
307 | }, $rel['searchname'].'.nfo', ['Content-type:' => 'application/octet-stream']); |
||
308 | } |
||
309 | |||
310 | echo nl2br(Utility::cp437toUTF($data['nfo'])); |
||
311 | } else { |
||
312 | return Utility::showApiError(300, 'Release does not have an NFO file associated.'); |
||
313 | } |
||
314 | } else { |
||
315 | return Utility::showApiError(300, 'Release does not exist.'); |
||
316 | } |
||
317 | break; |
||
318 | |||
319 | // Capabilities request. |
||
320 | case 'c': |
||
321 | $this->output([], $params, $outputXML, $offset, 'caps'); |
||
322 | break; |
||
323 | } |
||
324 | } |
||
325 | |||
326 | /** |
||
327 | * @throws \Exception |
||
328 | */ |
||
329 | public function output($data, array $params, bool $xml, int $offset, string $type = '') |
||
330 | { |
||
331 | $this->type = $type; |
||
332 | $options = [ |
||
333 | 'Parameters' => $params, |
||
334 | 'Data' => $data, |
||
335 | 'Server' => $this->getForMenu(), |
||
336 | 'Offset' => $offset, |
||
337 | 'Type' => $type, |
||
338 | ]; |
||
339 | |||
340 | // Generate the XML Response |
||
341 | $response = (new XML_Response($options))->returnXML(); |
||
342 | |||
343 | if ($xml) { |
||
344 | header('Content-type: text/xml'); |
||
345 | } else { |
||
346 | // JSON encode the XMLWriter response |
||
347 | $response = json_encode(xml_to_array($response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES); |
||
348 | header('Content-type: application/json'); |
||
349 | } |
||
350 | if ($response === false) { |
||
351 | return Utility::showApiError(201); |
||
352 | } else { |
||
353 | header('Content-Length: '.\strlen($response)); |
||
354 | echo $response; |
||
355 | exit; |
||
356 | } |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * Collect and return various capability information for usage in API. |
||
361 | * |
||
362 | * |
||
363 | * @throws \Exception |
||
364 | */ |
||
365 | public function getForMenu(): array |
||
366 | { |
||
367 | $serverroot = url('/'); |
||
368 | |||
369 | return [ |
||
370 | 'server' => [ |
||
371 | 'title' => config('app.name'), |
||
372 | 'strapline' => Settings::settingValue('strapline'), |
||
373 | 'email' => config('mail.from.address'), |
||
374 | 'meta' => Settings::settingValue('metakeywords'), |
||
375 | 'url' => $serverroot, |
||
376 | 'image' => $serverroot.'/assets/images/tmux_logo.png', |
||
377 | ], |
||
378 | 'limits' => [ |
||
379 | 'max' => 100, |
||
380 | 'default' => 100, |
||
381 | ], |
||
382 | 'registration' => [ |
||
383 | 'available' => 'yes', |
||
384 | 'open' => (int) Settings::settingValue('registerstatus') === 0 ? 'yes' : 'no', |
||
385 | ], |
||
386 | 'searching' => [ |
||
387 | 'search' => ['available' => 'yes', 'supportedParams' => 'q'], |
||
388 | 'tv-search' => ['available' => 'yes', 'supportedParams' => 'q,vid,tvdbid,traktid,rid,tvmazeid,imdbid,tmdbid,season,ep'], |
||
389 | 'movie-search' => ['available' => 'yes', 'supportedParams' => 'q,imdbid, tmdbid, traktid'], |
||
390 | 'audio-search' => ['available' => 'no', 'supportedParams' => ''], |
||
391 | ], |
||
392 | 'categories' => $this->type === 'caps' |
||
393 | ? Category::getForMenu() |
||
394 | : null, |
||
395 | ]; |
||
396 | } |
||
397 | |||
398 | /** |
||
399 | * @return Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\Response|int |
||
400 | */ |
||
401 | public function maxAge(Request $request) |
||
402 | { |
||
403 | $maxAge = -1; |
||
404 | if ($request->has('maxage')) { |
||
405 | if (! $request->filled('maxage')) { |
||
406 | return Utility::showApiError(201, 'Incorrect parameter (maxage must not be empty)'); |
||
407 | } elseif (! is_numeric($request->input('maxage'))) { |
||
408 | return Utility::showApiError(201, 'Incorrect parameter (maxage must be numeric)'); |
||
409 | } else { |
||
410 | $maxAge = (int) $request->input('maxage'); |
||
411 | } |
||
412 | } |
||
413 | |||
414 | return $maxAge; |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Verify cat parameter. |
||
419 | */ |
||
420 | public function categoryID(Request $request): array |
||
421 | { |
||
422 | $categoryID[] = -1; |
||
423 | if ($request->has('cat')) { |
||
424 | $categoryIDs = urldecode($request->input('cat')); |
||
425 | // Append Web-DL category ID if HD present for SickBeard / Sonarr compatibility. |
||
426 | if (str_contains($categoryIDs, (string) Category::TV_HD) && ! str_contains($categoryIDs, (string) Category::TV_WEBDL) && (int) Settings::settingValue('catwebdl') === 0) { |
||
427 | $categoryIDs .= (','.Category::TV_WEBDL); |
||
428 | } |
||
429 | $categoryID = explode(',', $categoryIDs); |
||
430 | } |
||
431 | |||
432 | return $categoryID; |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * Verify groupName parameter. |
||
437 | * |
||
438 | * |
||
439 | * @throws \Exception |
||
440 | */ |
||
441 | public function group(Request $request): string|int|bool |
||
442 | { |
||
443 | $groupName = -1; |
||
444 | if ($request->has('group')) { |
||
445 | $group = UsenetGroup::isValidGroup($request->input('group')); |
||
446 | if ($group !== false) { |
||
447 | $groupName = $group; |
||
448 | } |
||
449 | } |
||
450 | |||
451 | return $groupName; |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | * Verify limit parameter. |
||
456 | */ |
||
457 | public function limit(Request $request): int |
||
458 | { |
||
459 | $limit = 100; |
||
460 | if ($request->has('limit') && is_numeric($request->input('limit'))) { |
||
461 | $limit = (int) $request->input('limit'); |
||
462 | } |
||
463 | |||
464 | return $limit; |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * Verify offset parameter. |
||
469 | */ |
||
470 | public function offset(Request $request): int |
||
471 | { |
||
472 | $offset = 0; |
||
473 | if ($request->has('offset') && is_numeric($request->input('offset'))) { |
||
474 | $offset = (int) $request->input('offset'); |
||
475 | } |
||
476 | |||
477 | return $offset; |
||
478 | } |
||
479 | |||
480 | /** |
||
481 | * Check if a parameter is empty. |
||
482 | */ |
||
483 | public function verifyEmptyParameter(Request $request, string $parameter) |
||
484 | { |
||
485 | if ($request->has($parameter) && $request->isNotFilled($parameter)) { |
||
486 | return Utility::showApiError(201, 'Incorrect parameter ('.$parameter.' must not be empty)'); |
||
487 | } |
||
488 | } |
||
489 | |||
490 | public function addCoverURL(&$releases, callable $getCoverURL): void |
||
496 | } |
||
497 | } |
||
498 | } |
||
499 | } |
||
500 | } |
||
501 |