ApiController::api()   F
last analyzed

Complexity

Conditions 62
Paths > 20000

Size

Total Lines 289
Code Lines 213

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 213
c 2
b 1
f 0
dl 0
loc 289
rs 0
cc 62
nc 1618050
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Http\Controllers\Api;
4
5
use App\Events\UserAccessedApi;
6
use App\Http\Controllers\BasePageController;
7
use App\Models\Category;
8
use App\Models\Release;
9
use App\Models\ReleaseNfo;
10
use App\Models\Settings;
11
use App\Models\UsenetGroup;
12
use App\Models\User;
13
use App\Models\UserDownload;
14
use App\Models\UserRequest;
15
use Blacklight\Releases;
16
use Blacklight\utility\Utility;
17
use Illuminate\Contracts\Foundation\Application;
18
use Illuminate\Http\RedirectResponse;
19
use Illuminate\Http\Request;
20
use Illuminate\Routing\Redirector;
21
use Illuminate\Support\Carbon;
22
use Illuminate\Support\Str;
23
use Symfony\Component\HttpFoundation\StreamedResponse;
24
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;
0 ignored issues
show
Bug introduced by
The property apirequests does not seem to exist on Spatie\Permission\Models\Role.
Loading history...
100
            $maxDownloads = $res->role->downloadrequests;
0 ignored issues
show
Bug introduced by
The property downloadrequests does not seem to exist on Spatie\Permission\Models\Role.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $uid can also be of type string; however, parameter $userID of App\Models\UserRequest::getApiRequests() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
            $thisRequests = UserRequest::getApiRequests(/** @scrutinizer ignore-type */ $uid);
Loading history...
111
            $grabs = UserDownload::getDownloadRequests($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid can also be of type string; however, parameter $userID of App\Models\UserDownload::getDownloadRequests() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

111
            $grabs = UserDownload::getDownloadRequests(/** @scrutinizer ignore-type */ $uid);
Loading history...
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';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::search() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
                        /** @scrutinizer ignore-type */ $maxAge,
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::getBrowseRange() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
                        /** @scrutinizer ignore-type */ $maxAge,
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::tvSearch() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
                    /** @scrutinizer ignore-type */ $maxAge,
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::moviesSearch() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

254
                    /** @scrutinizer ignore-type */ $maxAge,
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type false; however, parameter $xml of xml_to_array() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

347
            $response = json_encode(xml_to_array(/** @scrutinizer ignore-type */ $response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);
Loading history...
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;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$categoryID was never initialized. Although not strictly required by PHP, it is generally a good practice to add $categoryID = array(); before regardless.
Loading history...
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
491
    {
492
        if ($releases && \count($releases)) {
493
            foreach ($releases as $key => $release) {
494
                if (isset($release->id)) {
495
                    $release->coverurl = $getCoverURL($release);
496
                }
497
            }
498
        }
499
    }
500
}
501