Completed
Push — dev ( 27415e...169927 )
by Darko
09:04
created

NZB::buildNZBPath()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

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

107
        if (! /** @scrutinizer ignore-deprecated */ ends_with($this->siteNzbPath, '/')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

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