Completed
Push — dev ( 928387...7c4d30 )
by Darko
10:29 queued 11s
created

NZB::setQueries()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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