Completed
Branch dev (4bcb34)
by Darko
13:52
created

NZB::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 16
nc 4
nop 1
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
3
namespace Blacklight;
4
5
use Blacklight\db\DB;
6
use App\Models\Settings;
7
use Blacklight\utility\Utility;
8
use App\Extensions\util\Versions;
9
10
/**
11
 * Class for reading and writing NZB files on the hard disk,
12
 * building folder paths to store the NZB files.
13
 */
14
class NZB
15
{
16
    const NZB_NONE = 0; // Release has no NZB file yet.
17
    const NZB_ADDED = 1; // Release had an NZB file created.
18
19
    const NZB_DTD_NAME = 'nzb';
20
    const NZB_DTD_PUBLIC = '-//newzBin//DTD NZB 1.1//EN';
21
    const NZB_DTD_EXTERNAL = 'http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd';
22
    const NZB_XML_NS = 'http://www.newzbin.com/DTD/2003/nzb';
23
24
    /**
25
     * Levels deep to store NZB files.
26
     *
27
     * @var int
28
     */
29
    protected $nzbSplitLevel;
30
31
    /**
32
     * Path to store NZB files.
33
     *
34
     * @var string
35
     */
36
    protected $siteNzbPath;
37
38
    /**
39
     * Group id when writing NZBs.
40
     *
41
     * @var int
42
     */
43
    protected $groupID;
44
45
    /**
46
     * @var \Blacklight\db\DB
47
     */
48
    public $pdo;
49
50
    /**
51
     * @var bool
52
     */
53
    protected $_debug = false;
54
55
    /**
56
     * Base query for selecting collection data for writing NZB files.
57
     *
58
     * @var string
59
     */
60
    protected $_collectionsQuery;
61
62
    /**
63
     * Base query for selecting binary data for writing NZB files.
64
     *
65
     * @var string
66
     */
67
    protected $_binariesQuery;
68
69
    /**
70
     * Base query for selecting parts data for writing NZB files.
71
     *
72
     * @var string
73
     */
74
    protected $_partsQuery;
75
76
    /**
77
     * String used for head in NZB XML file.
78
     *
79
     * @var string
80
     */
81
    protected $_nzbCommentString;
82
83
    /**
84
     * Names of CBP tables.
85
     *
86
     * @var array [string => string]
87
     */
88
    protected $_tableNames;
89
90
    /**
91
     * @var string
92
     */
93
    protected $_siteCommentString;
94
95
    /**
96
     * NZB constructor.
97
     *
98
     * @param array $options
99
     * @throws \Exception
100
     */
101
    public function __construct(array $options = [])
102
    {
103
        $defaults = [
104
            'Settings' => null,
105
        ];
106
        $options += $defaults;
107
108
        $this->pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB());
109
110
        $nzbSplitLevel = Settings::settingValue('..nzbsplitlevel');
0 ignored issues
show
Bug introduced by
'..nzbsplitlevel' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

110
        $nzbSplitLevel = Settings::settingValue(/** @scrutinizer ignore-type */ '..nzbsplitlevel');
Loading history...
111
        $this->nzbSplitLevel = $nzbSplitLevel ?? 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like $nzbSplitLevel ?? 1 can also be of type string. However, the property $nzbSplitLevel is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
112
        $this->siteNzbPath = (string) Settings::settingValue('..nzbpath');
113
        if (substr($this->siteNzbPath, -1) !== DS) {
0 ignored issues
show
Bug introduced by
The constant Blacklight\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
114
            $this->siteNzbPath .= DS;
115
        }
116
        $this->_nzbCommentString = sprintf(
117
            'NZB Generated by: NNTmux %s %s',
118
            (new Versions())->getGitTagInFile(),
119
            Utility::htmlfmt(date('F j, Y, g:i a O'))
120
        );
121
        $this->_siteCommentString = sprintf(
122
            'NZB downloaded from %s',
123
            Settings::settingValue('site.main.title')
124
        );
125
    }
126
127
    /**
128
     * Initiate class vars when writing NZB's.
129
     *
130
     * @param int $groupID
131
     */
132
    public function initiateForWrite($groupID)
133
    {
134
        $this->groupID = $groupID;
135
        // Set table names
136
137
        if ($this->groupID === '') {
0 ignored issues
show
introduced by
The condition $this->groupID === '' is always false.
Loading history...
138
            exit("{$this->groupID} is missing\n");
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...
139
        }
140
        $this->_tableNames = [
141
            'cName' => 'collections_'.$this->groupID,
142
            'bName' => 'binaries_'.$this->groupID,
143
            'pName' => 'parts_'.$this->groupID,
144
        ];
145
        $this->setQueries();
146
    }
147
148
    protected function setQueries(): void
149
    {
150
        $this->_collectionsQuery = "
151
			SELECT c.*, UNIX_TIMESTAMP(c.date) AS udate,
152
				g.name AS groupname
153
			FROM {$this->_tableNames['cName']} c
154
			INNER JOIN groups g ON c.groups_id = g.id
155
			WHERE c.releases_id = ";
156
        $this->_binariesQuery = "
157
			SELECT b.id, b.name, b.totalparts
158
			FROM {$this->_tableNames['bName']} b
159
			WHERE b.collections_id = %d
160
			ORDER BY b.name ASC";
161
        $this->_partsQuery = "
162
			SELECT DISTINCT(p.messageid), p.size, p.partnumber
163
			FROM {$this->_tableNames['pName']} p
164
			WHERE p.binaries_id = %d
165
			ORDER BY p.partnumber ASC";
166
    }
167
168
    /**
169
     * Write an NZB to the hard drive for a single release.
170
     *
171
     * @param int    $relID   The ID of the release in the DB.
172
     * @param string $relGuid The guid of the release.
173
     * @param string $name    The name of the release.
174
     * @param string $cTitle  The name of the category this release is in.
175
     *
176
     * @return bool Have we successfully written the NZB to the hard drive?
177
     */
178
    public function writeNZBforReleaseId($relID, $relGuid, $name, $cTitle): bool
179
    {
180
        $collections = $this->pdo->queryDirect($this->_collectionsQuery.$relID);
181
182
        if (! $collections instanceof \Traversable) {
183
            return false;
184
        }
185
186
        $XMLWriter = new \XMLWriter();
187
        $XMLWriter->openMemory();
188
        $XMLWriter->setIndent(true);
189
        $XMLWriter->setIndentString('  ');
190
191
        $nzb_guid = '';
192
193
        $XMLWriter->startDocument('1.0', 'UTF-8');
194
        $XMLWriter->startDTD(self::NZB_DTD_NAME, self::NZB_DTD_PUBLIC, self::NZB_DTD_EXTERNAL);
195
        $XMLWriter->endDTD();
196
        $XMLWriter->writeComment($this->_nzbCommentString);
197
198
        $XMLWriter->startElement('nzb');
199
        $XMLWriter->writeAttribute('xmlns', self::NZB_XML_NS);
200
        $XMLWriter->startElement('head');
201
        $XMLWriter->startElement('meta');
202
        $XMLWriter->writeAttribute('type', 'category');
203
        $XMLWriter->text($cTitle);
204
        $XMLWriter->endElement();
205
        $XMLWriter->startElement('meta');
206
        $XMLWriter->writeAttribute('type', 'name');
207
        $XMLWriter->text($name);
208
        $XMLWriter->endElement();
209
        $XMLWriter->endElement(); //head
210
211
        foreach ($collections as $collection) {
212
            $binaries = $this->pdo->queryDirect(sprintf($this->_binariesQuery, $collection['id']));
213
            if ($binaries === false) {
214
                return false;
215
            }
216
217
            $poster = $collection['fromname'];
218
219
            foreach ($binaries as $binary) {
220
                $parts = $this->pdo->queryDirect(sprintf($this->_partsQuery, $binary['id']));
221
                if ($parts === false) {
222
                    return false;
223
                }
224
225
                $subject = $binary['name'].'(1/'.$binary['totalparts'].')';
226
                $XMLWriter->startElement('file');
227
                $XMLWriter->writeAttribute('poster', $poster);
228
                $XMLWriter->writeAttribute('date', $collection['udate']);
229
                $XMLWriter->writeAttribute('subject', $subject);
230
                $XMLWriter->startElement('groups');
231
                if (preg_match_all('#(\S+):\S+#', $collection['xref'], $matches)) {
232
                    $matches = array_unique($matches[1]);
233
                    foreach ($matches as $group) {
234
                        $XMLWriter->writeElement('group', $group);
235
                    }
236
                } else {
237
                    return false;
238
                }
239
                $XMLWriter->endElement(); //groups
240
                $XMLWriter->startElement('segments');
241
                foreach ($parts as $part) {
242
                    if ($nzb_guid === '') {
243
                        $nzb_guid = $part['messageid'];
244
                    }
245
                    $XMLWriter->startElement('segment');
246
                    $XMLWriter->writeAttribute('bytes', $part['size']);
247
                    $XMLWriter->writeAttribute('number', $part['partnumber']);
248
                    $XMLWriter->text($part['messageid']);
249
                    $XMLWriter->endElement();
250
                }
251
                $XMLWriter->endElement(); //segments
252
                $XMLWriter->endElement(); //file
253
            }
254
        }
255
        $XMLWriter->writeComment($this->_siteCommentString);
256
        $XMLWriter->endElement(); //nzb
257
        $XMLWriter->endDocument();
258
        $path = ($this->buildNZBPath($relGuid, $this->nzbSplitLevel, true).$relGuid.'.nzb.gz');
259
        $fp = gzopen($path, 'wb7');
260
        if (! $fp) {
0 ignored issues
show
introduced by
The condition $fp is always false.
Loading history...
261
            return false;
262
        }
263
        gzwrite($fp, $XMLWriter->outputMemory());
264
        gzclose($fp);
265
        unset($XMLWriter);
266
        if (! is_file($path)) {
267
            echo "ERROR: $path does not exist.\n";
268
269
            return false;
270
        }
271
        // Mark release as having NZB.
272
        $this->pdo->queryExec(
273
            sprintf(
274
                '
275
				UPDATE releases SET nzbstatus = %d %s WHERE id = %d',
276
                self::NZB_ADDED,
277
                ($nzb_guid === '' ? '' : ', nzb_guid = UNHEX( '.$this->pdo->escapeString(md5($nzb_guid)).' )'),
278
                $relID
279
            )
280
        );
281
        // Delete CBP for release that has its NZB created.
282
        $this->pdo->queryExec(
283
            sprintf(
284
                '
285
				DELETE c, b, p FROM %s c JOIN %s b ON(c.id=b.collections_id) STRAIGHT_JOIN %s p ON(b.id=p.binaries_id) WHERE c.releases_id = %d',
286
                $this->_tableNames['cName'],
287
                $this->_tableNames['bName'],
288
                $this->_tableNames['pName'],
289
                $relID
290
            )
291
        );
292
        // Chmod to fix issues some users have with file permissions.
293
        chmod($path, 0777);
294
295
        return true;
296
    }
297
298
    /**
299
     * Build a folder path on the hard drive where the NZB file will be stored.
300
     *
301
     * @param string $releaseGuid      The guid of the release.
302
     * @param int    $levelsToSplit    How many sub-paths the folder will be in.
303
     * @param bool   $createIfNotExist Create the folder if it doesn't exist.
304
     *
305
     * @return string $nzbpath The path to store the NZB file.
306
     */
307
    public function buildNZBPath($releaseGuid, $levelsToSplit, $createIfNotExist)
308
    {
309
        $nzbPath = '';
310
311
        for ($i = 0; $i < $levelsToSplit && $i < 32; $i++) {
312
            $nzbPath .= substr($releaseGuid, $i, 1).DS;
0 ignored issues
show
Bug introduced by
The constant Blacklight\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
313
        }
314
315
        $nzbPath = $this->siteNzbPath.$nzbPath;
316
317
        if ($createIfNotExist === true && ! is_dir($nzbPath)) {
318
            mkdir($nzbPath, 0777, true);
319
        }
320
321
        return $nzbPath;
322
    }
323
324
    /**
325
     * Retrieve path + filename of the NZB to be stored.
326
     *
327
     * @param string $releaseGuid      The guid of the release.
328
     * @param int    $levelsToSplit    How many sub-paths the folder will be in. (optional)
329
     * @param bool   $createIfNotExist Create the folder if it doesn't exist. (optional)
330
     *
331
     * @return string Path+filename.
332
     */
333
    public function getNZBPath($releaseGuid, $levelsToSplit = 0, $createIfNotExist = false): string
334
    {
335
        if ($levelsToSplit === 0) {
336
            $levelsToSplit = $this->nzbSplitLevel;
337
        }
338
339
        return $this->buildNZBPath($releaseGuid, $levelsToSplit, $createIfNotExist).$releaseGuid.'.nzb.gz';
340
    }
341
342
    /**
343
     * Determine is an NZB exists, returning the path+filename, if not return false.
344
     *
345
     * @param  string $releaseGuid The guid of the release.
346
     *
347
     * @return bool|string On success: (string) Path+file name of the nzb.
348
     *                     On failure: (bool)   False.
349
     */
350
    public function NZBPath($releaseGuid)
351
    {
352
        $nzbFile = $this->getNZBPath($releaseGuid);
353
354
        return is_file($nzbFile) ? $nzbFile : false;
355
    }
356
357
    /**
358
     * Retrieve various information on a NZB file (the subject, # of pars,
359
     * file extensions, file sizes, file completion, group names, # of parts).
360
     *
361
     * @param string $nzb The NZB contents in a string.
362
     * @param array  $options
363
     *                    'no-file-key'    => True - use numeric array key; False - Use filename as array key.
364
     *                    'strip-count'    => True - Strip file/part count from file name to make the array key; False - Leave file name as is.
365
     *
366
     * @return array $result Empty if not an NZB or the contents of the NZB.
367
     */
368
    public function nzbFileList($nzb, array $options = []): array
369
    {
370
        $defaults = [
371
            'no-file-key' => true,
372
            'strip-count' => false,
373
        ];
374
        $options += $defaults;
375
376
        $num_pars = $i = 0;
377
        $result = [];
378
379
        if (! $nzb) {
380
            return $result;
381
        }
382
383
        $xml = @simplexml_load_string(str_replace("\x0F", '', $nzb));
384
        if (! $xml || strtolower($xml->getName()) !== 'nzb') {
385
            return $result;
386
        }
387
388
        foreach ($xml->file as $file) {
389
            // Subject.
390
            $title = (string) $file->attributes()->subject;
391
392
            // Amount of pars.
393
            if (stripos($title, '.par2')) {
394
                $num_pars++;
395
            }
396
397
            if ($options['no-file-key'] === false) {
398
                $i = $title;
399
                if ($options['strip-count']) {
400
                    // Strip file / part count to get proper sorting.
401
                    $i = preg_replace('#\d+[- ._]?(/|\||[o0]f)[- ._]?\d+?(?![- ._]\d)#i', '', $i);
402
                    // Change .rar and .par2 to be sorted before .part0x.rar and .volxxx+xxx.par2
403
                    if (strpos($i, '.par2') !== false && ! preg_match('#\.vol\d+\+\d+\.par2#i', $i)) {
404
                        $i = str_replace('.par2', '.vol0.par2', $i);
405
                    } elseif (preg_match('#\.rar[^a-z0-9]#i', $i) && ! preg_match('#\.part\d+\.rar#i', $i)) {
406
                        $i = preg_replace('#\.rar(?:[^a-z0-9])#i', '.part0.rar', $i);
407
                    }
408
                }
409
            }
410
411
            $result[$i]['title'] = $title;
412
413
            // Extensions.
414
            if (preg_match(
415
                '/\.(\d{2,3}|7z|ace|ai7|srr|srt|sub|aiff|asc|avi|audio|bin|bz2|'
416
                .'c|cfc|cfm|chm|class|conf|cpp|cs|css|csv|cue|deb|divx|doc|dot|'
417
                .'eml|enc|exe|file|gif|gz|hlp|htm|html|image|iso|jar|java|jpeg|'
418
                .'jpg|js|lua|m|m3u|mkv|mm|mov|mp3|mp4|mpg|nfo|nzb|odc|odf|odg|odi|odp|'
419
                .'ods|odt|ogg|par2|parity|pdf|pgp|php|pl|png|ppt|ps|py|r\d{2,3}|'
420
                .'ram|rar|rb|rm|rpm|rtf|sfv|sig|sql|srs|swf|sxc|sxd|sxi|sxw|tar|'
421
                .'tex|tgz|txt|vcf|video|vsd|wav|wma|wmv|xls|xml|xpi|xvid|zip7|zip)'
422
                .'[" ](?!(\)|\-))/i',
423
                $title,
424
                $ext
425
            )
426
            ) {
427
                if (preg_match('/\.r\d{2,3}/i', $ext[0])) {
428
                    $ext[1] = 'rar';
429
                }
430
                $result[$i]['ext'] = strtolower($ext[1]);
431
            } else {
432
                $result[$i]['ext'] = '';
433
            }
434
435
            $fileSize = $numSegments = 0;
436
437
            // Parts.
438
            if (! isset($result[$i]['segments'])) {
439
                $result[$i]['segments'] = [];
440
            }
441
442
            // File size.
443
            foreach ($file->segments->segment as $segment) {
444
                $result[$i]['segments'][] = (string) $segment;
445
                $fileSize += $segment->attributes()->bytes;
446
                $numSegments++;
447
            }
448
            $result[$i]['size'] = $fileSize;
449
450
            // File completion.
451
            if (preg_match('/(\d+)\)$/', $title, $parts)) {
452
                $result[$i]['partstotal'] = $parts[1];
453
            }
454
            $result[$i]['partsactual'] = $numSegments;
455
456
            // Groups.
457
            if (! isset($result[$i]['groups'])) {
458
                $result[$i]['groups'] = [];
459
            }
460
            foreach ($file->groups->group as $g) {
461
                $result[$i]['groups'][] = (string) $g;
462
            }
463
464
            unset($result[$i]['segments']['@attributes']);
465
            if ($options['no-file-key']) {
466
                $i++;
467
            }
468
        }
469
470
        return $result;
471
    }
472
}
473