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

NNTP   F

Complexity

Total Complexity 174

Size/Duplication

Total Lines 1201
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1201
rs 0.6314
c 0
b 0
f 0
wmc 174

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getOverview() 0 11 2
A __construct() 0 16 4
C get_Article() 0 64 13
D getMessages() 0 106 21
C getXOVER() 0 71 12
C _enableCompression() 0 37 7
A _getTextResponse() 0 9 4
B selectGroup() 0 15 5
A getGroups() 0 6 1
D _checkConnection() 0 33 9
A dataError() 0 21 4
A __destruct() 0 3 1
A doQuit() 0 11 3
A enableCompression() 0 6 2
C postArticle() 0 48 9
A _splitLines() 0 10 3
A _formatMessageID() 0 18 4
A _resetProperties() 0 7 1
F doConnect() 0 142 35
C _getMessage() 0 59 11
C get_Header() 0 47 10
D _getXFeatureTextResponse() 0 94 13

How to fix   Complexity   

Complex Class

Complex classes like NNTP 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 NNTP, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Blacklight;
4
5
use Blacklight\db\DB;
6
use App\Models\Settings;
7
use App\Extensions\util\Yenc;
8
9
/**
10
 * Class for connecting to the usenet, retrieving articles and article headers,
11
 * decoding yEnc articles, decompressing article headers.
12
 * Extends PEAR's Net_NNTP_Client class, overrides some functions.
13
 */
14
class NNTP extends \Net_NNTP_Client
15
{
16
    /**
17
     * @var \Blacklight\db\DB
18
     */
19
    public $pdo;
20
21
    /**
22
     * @var
23
     */
24
    protected $_colorCLI;
25
26
    /**
27
     * @var \Blacklight\Logger
0 ignored issues
show
Bug introduced by
The type Blacklight\Logger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
     */
29
    protected $_debugging;
30
31
    /**
32
     * @var bool
33
     */
34
    protected $_debugBool;
35
36
    /**
37
     * @var bool
38
     */
39
    protected $_echo;
40
41
    /**
42
     * Does the server support XFeature GZip header compression?
43
     * @var bool
44
     */
45
    protected $_compressionSupported = true;
46
47
    /**
48
     * Is header compression enabled for the session?
49
     * @var bool
50
     */
51
    protected $_compressionEnabled = false;
52
53
    /**
54
     * Currently selected group.
55
     * @var string
56
     */
57
    protected $_currentGroup = '';
58
59
    /**
60
     * Port of the current NNTP server.
61
     * @var int
62
     */
63
    protected $_currentPort = 'NNTP_PORT';
64
65
    /**
66
     * Address of the current NNTP server.
67
     * @var string
68
     */
69
    protected $_currentServer = 'NNTP_SERVER';
70
71
    /**
72
     * Are we allowed to post to usenet?
73
     * @var bool
74
     */
75
    protected $_postingAllowed = false;
76
77
    /**
78
     * How many times should we try to reconnect to the NNTP server?
79
     * @var int
80
     */
81
    protected $_nntpRetries;
82
83
    /**
84
     * Default constructor.
85
     *
86
     * @param array $options Class instances and echo to CLI bool.
87
     *
88
     * @throws \Exception
89
     */
90
    public function __construct(array $options = [])
91
    {
92
        $defaults = [
93
            'Echo'      => true,
94
            'Logger' => null,
95
            'Settings'  => null,
96
        ];
97
        $options += $defaults;
98
99
        parent::__construct();
100
101
        $this->_echo = ($options['Echo'] && config('nntmux.echocli'));
102
103
        $this->pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB());
104
105
        $this->_nntpRetries = Settings::settingValue('..nntpretries') !== '' ? (int) Settings::settingValue('..nntpretries') : 0 + 1;
0 ignored issues
show
Bug introduced by
'..nntpretries' 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

105
        $this->_nntpRetries = Settings::settingValue(/** @scrutinizer ignore-type */ '..nntpretries') !== '' ? (int) Settings::settingValue('..nntpretries') : 0 + 1;
Loading history...
106
    }
107
108
    /**
109
     * Destruct.
110
     * Close the NNTP connection if still connected.
111
     */
112
    public function __destruct()
113
    {
114
        $this->doQuit();
115
    }
116
117
    /**
118
     * Connect to a usenet server.
119
     *
120
     * @param bool $compression Should we attempt to enable XFeature Gzip compression on this connection?
121
     * @param bool $alternate   Use the alternate NNTP connection.
122
     *
123
     * @return mixed  On success = (bool)   Did we successfully connect to the usenet?
124
     * @throws \Exception
125
     *                On failure = (object) PEAR_Error.
126
     */
127
    public function doConnect($compression = true, $alternate = false)
128
    {
129
        if (// (Alternate is wanted, AND current server is alt,  OR  Alternate is not wanted AND current is main.)         AND
130
        (($alternate && $this->_currentServer === env('NNTP_SERVER_A')) || (! $alternate && $this->_currentServer === env('NNTP_SERVER'))) &&
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: {currentAssign}, Probably Intended Meaning: {alternativeAssign}
Loading history...
131
            // Don't reconnect to usenet if:
132
            // We are already connected to usenet.
133
            parent::_isConnected()
134
135
        ) {
136
            return true;
137
        }
138
139
        $this->doQuit();
140
141
        $ret = $connected = $cError = $aError = false;
142
143
        // Set variables to connect based on if we are using the alternate provider or not.
144
        if (! $alternate) {
145
            $sslEnabled = env('NNTP_SSLENABLED') ? true : false;
146
            $this->_currentServer = env('NNTP_SERVER');
147
            $this->_currentPort = env('NNTP_PORT');
148
            $userName = env('NNTP_USERNAME');
149
            $password = env('NNTP_PASSWORD');
150
            $socketTimeout = ! empty(env('NNTP_SOCKET_TIMEOUT')) ? env('NNTP_SOCKET_TIMEOUT') : $this->_socketTimeout;
151
        } else {
152
            $sslEnabled = env('NNTP_SSLENABLED_A') ? true : false;
153
            $this->_currentServer = env('NNTP_SERVER_A');
154
            $this->_currentPort = env('NNTP_PORT_A');
155
            $userName = env('NNTP_USERNAME_A');
156
            $password = env('NNTP_PASSWORD_A');
157
            $socketTimeout = ! empty(env('NNTP_SOCKET_TIMEOUT_A')) ? env('NNTP_SOCKET_TIMEOUT_A') : $this->_socketTimeout;
158
        }
159
160
        $enc = ($sslEnabled ? ' (ssl)' : ' (non-ssl)');
161
        $sslEnabled = ($sslEnabled ? 'tls' : false);
162
163
        // Try to connect until we run of out tries.
164
        $retries = $this->_nntpRetries;
165
        while (true) {
166
            $retries--;
167
            $authenticated = false;
168
169
            // If we are not connected, try to connect.
170
            if (! $connected) {
171
                $ret = $this->connect($this->_currentServer, $sslEnabled, $this->_currentPort, 5, $socketTimeout);
0 ignored issues
show
Bug introduced by
It seems like $this->_currentServer can also be of type array; however, parameter $host of Net_NNTP_Client::connect() 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

171
                $ret = $this->connect(/** @scrutinizer ignore-type */ $this->_currentServer, $sslEnabled, $this->_currentPort, 5, $socketTimeout);
Loading history...
172
            }
173
174
            // Check if we got an error while connecting.
175
            $cErr = $this->isError($ret);
176
177
            // If no error, we are connected.
178
            if (! $cErr) {
179
                // Say that we are connected so we don't retry.
180
                $connected = true;
181
                // When there is no error it returns bool if we are allowed to post or not.
182
                $this->_postingAllowed = $ret;
183
            } else {
184
                // Only fetch the message once.
185
                if (! $cError) {
186
                    $cError = $ret->getMessage();
187
                }
188
            }
189
190
            // If error, try to connect again.
191
            if ($cErr && $retries > 0) {
192
                continue;
193
            }
194
195
            // If we have no more retries and could not connect, return an error.
196
            if ($retries === 0 && ! $connected) {
197
                $message =
198
                    'Cannot connect to server '.
199
                    $this->_currentServer.
200
                    $enc.
201
                    ': '.
202
                    $cError;
203
204
                return $this->throwError(ColorCLI::error($message));
205
            }
206
207
            // If we are connected, try to authenticate.
208
            if ($connected === true && $authenticated === false) {
209
210
                // If the username is empty it probably means the server does not require a username.
211
                if ($userName === '') {
212
                    $authenticated = true;
213
214
                // Try to authenticate to usenet.
215
                } else {
216
                    $ret2 = $this->authenticate($userName, $password);
0 ignored issues
show
Bug introduced by
It seems like $password can also be of type array; however, parameter $pass of Net_NNTP_Client::authenticate() 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

216
                    $ret2 = $this->authenticate($userName, /** @scrutinizer ignore-type */ $password);
Loading history...
217
218
                    // Check if there was an error authenticating.
219
                    $aErr = $this->isError($ret2);
220
221
                    // If there was no error, then we are authenticated.
222
                    if (! $aErr) {
223
                        $authenticated = true;
224
                    } elseif (! $aError) {
225
                        $aError = $ret2->getMessage();
226
                    }
227
228
                    // If error, try to authenticate again.
229
                    if ($aErr && $retries > 0) {
230
                        continue;
231
                    }
232
233
                    // If we ran out of retries, return an error.
234
                    if ($retries === 0 && $authenticated === false) {
235
                        $message =
236
                            'Cannot authenticate to server '.
237
                            $this->_currentServer.
238
                            $enc.
239
                            ' - '.
240
                            $userName.
241
                            ' ('.$aError.')';
242
243
                        return $this->throwError(ColorCLI::error($message));
244
                    }
245
                }
246
            }
247
248
            // If we are connected and authenticated, try enabling compression if we have it enabled.
249
            if ($connected === true && $authenticated === true) {
250
                // Check if we should use compression on the connection.
251
                if ($compression === false || (int) Settings::settingValue('..compressedheaders') === 0) {
0 ignored issues
show
Bug introduced by
'..compressedheaders' 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

251
                if ($compression === false || (int) Settings::settingValue(/** @scrutinizer ignore-type */ '..compressedheaders') === 0) {
Loading history...
252
                    $this->_compressionSupported = false;
253
                }
254
255
                return true;
256
            }
257
            // If we reached this point and have not connected after all retries, break out of the loop.
258
            if ($retries === 0) {
259
                break;
260
            }
261
262
            // Sleep .4 seconds between retries.
263
            usleep(400000);
264
        }
265
        // If we somehow got out of the loop, return an error.
266
        $message = 'Unable to connect to '.$this->_currentServer.$enc;
267
268
        return $this->throwError(ColorCLI::error($message));
269
    }
270
271
    /**
272
     * Disconnect from the current NNTP server.
273
     *
274
     * @param  bool $force Force quit even if not connected?
275
     *
276
     * @return mixed On success : (bool)   Did we successfully disconnect from usenet?
277
     *               On Failure : (object) PEAR_Error.
278
     */
279
    public function doQuit($force = false)
280
    {
281
        $this->_resetProperties();
282
283
        // Check if we are connected to usenet.
284
        if ($force === true || parent::_isConnected(false)) {
285
            // Disconnect from usenet.
286
            return parent::disconnect();
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * Reset some properties when disconnecting from usenet.
294
     *
295
     * @void
296
     */
297
    protected function _resetProperties(): void
298
    {
299
        $this->_compressionEnabled = false;
300
        $this->_compressionSupported = true;
301
        $this->_currentGroup = '';
302
        $this->_postingAllowed = false;
303
        parent::_resetProperties();
304
    }
305
306
    /**
307
     * Attempt to enable compression if the admin enabled the site setting.
308
     *
309
     * @note   This can be used to enable compression if the server was connected without compression.
310
     *
311
     * @throws \Exception
312
     */
313
    public function enableCompression(): void
314
    {
315
        if ((int) Settings::settingValue('..compressedheaders') !== 1) {
0 ignored issues
show
Bug introduced by
'..compressedheaders' 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

315
        if ((int) Settings::settingValue(/** @scrutinizer ignore-type */ '..compressedheaders') !== 1) {
Loading history...
316
            return;
317
        }
318
        $this->_enableCompression();
319
    }
320
321
    /**
322
     * @param string $group    Name of the group to select.
323
     * @param bool   $articles (optional) experimental! When true the article numbers is returned in 'articles'.
324
     * @param bool   $force    Force a refresh to get updated data from the usenet server.
325
     *
326
     * @return mixed On success : (array)  Group information.
327
     * @throws \Exception
328
     *               On failure : (object) PEAR_Error.
329
     */
330
    public function selectGroup($group, $articles = false, $force = false)
331
    {
332
        $connected = $this->_checkConnection(false);
333
        if ($connected !== true) {
334
            return $connected;
335
        }
336
337
        // Check if the current selected group is the same, or if we have not selected a group or if a fresh summary is wanted.
338
        if ($force || $this->_currentGroup !== $group || $this->_selectedGroupSummary === null) {
339
            $this->_currentGroup = $group;
340
341
            return parent::selectGroup($group, $articles);
342
        }
343
344
        return $this->_selectedGroupSummary;
345
    }
346
347
    /**
348
     * Fetch an overview of article(s) in the currently selected group.
349
     *
350
     * @param string $range
351
     * @param bool   $names
352
     * @param bool   $forceNames
353
     *
354
     * @return mixed On success : (array)  Multidimensional array with article headers.
355
     * @throws \Exception
356
     *               On failure : (object) PEAR_Error.
357
     */
358
    public function getOverview($range = null, $names = true, $forceNames = true)
359
    {
360
        $connected = $this->_checkConnection();
361
        if ($connected !== true) {
362
            return $connected;
363
        }
364
365
        // Enabled header compression if not enabled.
366
        $this->_enableCompression();
367
368
        return parent::getOverview($range, $names, $forceNames);
369
    }
370
371
    /**
372
     * Pass a XOVER command to the NNTP provider, return array of articles using the overview format as array keys.
373
     *
374
     * @note This is a faster implementation of getOverview.
375
     *
376
     * Example successful return:
377
     *    array(9) {
378
     *        'Number'     => string(9)  "679871775"
379
     *        'Subject'    => string(18) "This is an example"
380
     *        'From'       => string(19) "[email protected]"
381
     *        'Date'       => string(24) "26 Jun 2014 13:08:22 GMT"
382
     *        'Message-ID' => string(57) "<part1of1.uS*yYxQvtAYt$5t&wmE%[email protected]>"
383
     *        'References' => string(0)  ""
384
     *        'Bytes'      => string(3)  "123"
385
     *        'Lines'      => string(1)  "9"
386
     *        'Xref'       => string(66) "e alt.test:679871775"
387
     *    }
388
     *
389
     * @param string $range Range of articles to get the overview for. Examples follow:
390
     *                      Single article number:         "679871775"
391
     *                      Range of article numbers:      "679871775-679999999"
392
     *                      All newer than article number: "679871775-"
393
     *                      All older than article number: "-679871775"
394
     *                      Message-ID:                    "<part1of1.uS*yYxQvtAYt$5t&wmE%[email protected]>"
395
     *
396
     * @return array|object Multi-dimensional Array of headers on success, PEAR object on failure.
397
     * @throws \Exception
398
     */
399
    public function getXOVER($range)
400
    {
401
        // Check if we are still connected.
402
        $connected = $this->_checkConnection();
403
        if ($connected !== true) {
404
            return $connected;
405
        }
406
407
        // Enabled header compression if not enabled.
408
        $this->_enableCompression();
409
410
        // Send XOVER command to NNTP with wanted articles.
411
        $response = $this->_sendCommand('XOVER '.$range);
412
        if ($this->isError($response)) {
413
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response also could return the type integer which is incompatible with the documented return type object|array.
Loading history...
414
        }
415
416
        // Verify the NNTP server got the right command, get the headers data.
417
        if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_OVERVIEW_FOLLOWS) {
0 ignored issues
show
Bug introduced by
The constant Blacklight\NET_NNTP_PROT...SECODE_OVERVIEW_FOLLOWS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
418
            $data = $this->_getTextResponse();
419
            if ($this->isError($data)) {
420
                return $data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data returns the type string|Blacklight\NNTP which is incompatible with the documented return type object|array.
Loading history...
421
            }
422
        } else {
423
            return $this->_handleErrorResponse($response);
424
        }
425
426
        // Fetch the header overview format (for setting the array keys on the return array).
427
        if ($this->_overviewFormatCache !== null && isset($this->_overviewFormatCache['Xref'])) {
428
            $overview = $this->_overviewFormatCache;
429
        } else {
430
            $overview = $this->getOverviewFormat(false, true);
431
            if ($this->isError($overview)) {
432
                return $overview;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $overview also could return the type integer|string|Blacklight\NNTP which is incompatible with the documented return type object|array.
Loading history...
433
            }
434
            $this->_overviewFormatCache = $overview;
435
        }
436
        // Add the "Number" key.
437
        $overview = array_merge(['Number' => false], $overview);
438
439
        // Iterator used for selecting the header elements to insert into the overview format array.
440
        $iterator = 0;
441
442
        // Loop over strings of headers.
443
        foreach ($data as $key => $header) {
444
445
            // Split the individual headers by tab.
446
            $header = explode("\t", $header);
447
448
            // Make sure it's not empty.
449
            if ($header === false) {
450
                continue;
451
            }
452
453
            // Temp array to store the header.
454
            $headerArray = $overview;
455
456
            // Loop over the overview format and insert the individual header elements.
457
            foreach ($overview as $name => $element) {
458
                // Strip Xref:
459
                if ($element === true) {
460
                    $header[$iterator] = substr($header[$iterator], 6);
461
                }
462
                $headerArray[$name] = $header[$iterator++];
463
            }
464
            // Add the individual header array back to the return array.
465
            $data[$key] = $headerArray;
466
            $iterator = 0;
467
        }
468
        // Return the array of headers.
469
        return $data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data returns the type string|Blacklight\NNTP which is incompatible with the documented return type object|array.
Loading history...
470
    }
471
472
    /**
473
     * Fetch valid groups.
474
     *
475
     * Returns a list of valid groups (that the client is permitted to select) and associated information.
476
     *
477
     * @param string $wildMat (optional) http://tools.ietf.org/html/rfc3977#section-4
478
     *
479
     * @return array|object Pear error on failure, array with groups on success.
480
     */
481
    public function getGroups($wildMat = null)
482
    {
483
        // Enabled header compression if not enabled.
484
        $this->_enableCompression();
485
486
        return parent::getGroups($wildMat);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::getGroups($wildMat) also could return the type integer which is incompatible with the documented return type object|array.
Loading history...
487
    }
488
489
    /**
490
     * Download multiple article bodies and string them together.
491
     *
492
     * @param string $groupName   The name of the group the articles are in.
493
     * @param mixed  $identifiers (string) Message-ID.
494
     *                            (int)    Article number.
495
     *                            (array)  Article numbers or Message-ID's (can contain both in the same array)
496
     * @param bool   $alternate   Use the alternate NNTP provider?
497
     *
498
     * @return mixed On success : (string) The article bodies.
499
     * @throws \Exception
500
     *               On failure : (object) PEAR_Error.
501
     */
502
    public function getMessages($groupName, $identifiers, $alternate = false)
503
    {
504
        $connected = $this->_checkConnection();
505
        if ($connected !== true) {
506
            return $connected;
507
        }
508
509
        // String to hold all the bodies.
510
        $body = '';
511
512
        $aConnected = false;
513
        $nntp = ($alternate === true ? new self(['Echo' => $this->_echo, 'Settings' => $this->pdo]) : null);
514
515
        // Check if the msgIds are in an array.
516
        if (\is_array($identifiers)) {
517
            $loops = $messageSize = 0;
518
519
            // Loop over the message-ID's or article numbers.
520
            foreach ($identifiers as $wanted) {
521
522
                /* This is to attempt to prevent string size overflow.
523
                 * We get the size of 1 body in bytes, we increment the loop on every loop,
524
                 * then we multiply the # of loops by the first size we got and check if it
525
                 * exceeds 1.7 billion bytes (less than 2GB to give us headroom).
526
                 * If we exceed, return the data.
527
                 * If we don't do this, these errors are fatal.
528
                 */
529
                if ((++$loops * $messageSize) >= 1700000000) {
530
                    return $body;
531
                }
532
533
                // Download the body.
534
                $message = $this->_getMessage($groupName, $wanted);
535
536
                // Append the body to $body.
537
                if (! $this->isError($message)) {
538
                    $body .= $message;
539
540
                    if ($messageSize === 0) {
541
                        $messageSize = \strlen($message);
542
                    }
543
544
                    // If there is an error try the alternate provider or return the PEAR error.
545
                } else {
546
                    // Check if admin has enabled alternate in site->edit.
547
                    if ($alternate === true) {
548
                        if ($aConnected === false) {
549
                            // Check if the current connected server is the alternate or not.
550
                            if ($this->_currentServer === env('NNTP_SERVER')) {
551
                                // It's the main so connect to the alternate.
552
                                $aConnected = $nntp->doConnect(true, true);
0 ignored issues
show
Bug introduced by
The method doConnect() does not exist on null. ( Ignorable by Annotation )

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

552
                                /** @scrutinizer ignore-call */ 
553
                                $aConnected = $nntp->doConnect(true, true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
553
                            } else {
554
                                // It's the alternate so connect to the main.
555
                                $aConnected = $nntp->doConnect();
556
                            }
557
                        }
558
                        // If we connected successfully to usenet try to download the article body.
559
                        if ($aConnected === true) {
560
                            $newBody = $nntp->_getMessage($groupName, $wanted);
561
                            // Check if we got an error.
562
                            if ($nntp->isError($newBody)) {
563
                                if ($aConnected) {
564
                                    $nntp->doQuit();
565
                                }
566
                                // If we got some data, return it.
567
                                if ($body !== '') {
568
                                    return $body;
569
                                }
570
                                // Return the error.
571
                                return $newBody;
572
                            }
573
                            // Append the alternate body to the main body.
574
                            $body .= $newBody;
575
                        }
576
                    } else {
577
                        // If we got some data, return it.
578
                        if ($body !== '') {
579
                            return $body;
580
                        }
581
582
                        return $message;
583
                    }
584
                }
585
            }
586
587
            // If it's a string check if it's a valid message-ID.
588
        } elseif (\is_string($identifiers) || is_numeric($identifiers)) {
589
            $body = $this->_getMessage($groupName, $identifiers);
590
            if ($alternate === true && $this->isError($body)) {
591
                $nntp->doConnect(true, true);
592
                $body = $nntp->_getMessage($groupName, $identifiers);
593
                $aConnected = true;
594
            }
595
596
            // Else return an error.
597
        } else {
598
            $message = 'Wrong Identifier type, array, int or string accepted. This type of var was passed: '.gettype($identifiers);
599
600
            return $this->throwError(ColorCLI::error($message));
601
        }
602
603
        if ($aConnected === true) {
604
            $nntp->doQuit();
605
        }
606
607
        return $body;
608
    }
609
610
    /**
611
     * Download a full article, the body and the header, return an array with named keys and their
612
     * associated values, optionally decode the body using yEnc.
613
     *
614
     * @param string $groupName The name of the group the article is in.
615
     * @param mixed $identifier (string)The message-ID of the article to download.
616
     *                           (int) The article number.
617
     * @param bool $yEnc Attempt to yEnc decode the body.
618
     *
619
     * @return mixed  On success : (array)  The article.
620
     *                On failure : (object) PEAR_Error.
621
     * @throws \Exception
622
     */
623
    public function get_Article($groupName, $identifier, $yEnc = false)
624
    {
625
        $connected = $this->_checkConnection();
626
        if ($connected !== true) {
627
            return $connected;
628
        }
629
630
        // Make sure the requested group is already selected, if not select it.
631
        if (parent::group() !== $groupName) {
632
            // Select the group.
633
            $summary = $this->selectGroup($groupName);
634
            // If there was an error selecting the group, return PEAR error object.
635
            if ($this->isError($summary)) {
636
                return $summary;
637
            }
638
        }
639
640
        // Check if it's an article number or message-ID.
641
        if (! is_numeric($identifier)) {
642
            // If it's a message-ID, check if it has the required triangular brackets.
643
            $identifier = $this->_formatMessageID($identifier);
644
        }
645
646
        // Download the article.
647
        $article = parent::getArticle($identifier);
648
        // If there was an error downloading the article, return a PEAR error object.
649
        if ($this->isError($article)) {
650
            return $article;
651
        }
652
653
        $ret = $article;
654
        // Make sure the article is an array and has more than 1 element.
655
        if (\count($article) > 0) {
0 ignored issues
show
Bug introduced by
It seems like $article can also be of type integer; however, parameter $var of count() does only seem to accept Countable|array, 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

655
        if (\count(/** @scrutinizer ignore-type */ $article) > 0) {
Loading history...
656
            $ret = [];
657
            $body = '';
658
            $emptyLine = false;
659
            foreach ($article as $line) {
660
                // If we found the empty line it means we are done reading the header and we will start reading the body.
661
                if (! $emptyLine) {
662
                    if ($line === '') {
663
                        $emptyLine = true;
664
                        continue;
665
                    }
666
667
                    // Use the line type of the article as the array key (From, Subject, etc..).
668
                    if (preg_match('/([A-Z-]+?): (.*)/i', $line, $matches)) {
669
                        // If the line type takes more than 1 line, append the rest of the content to the same key.
670
                        if (array_key_exists($matches[1], $ret)) {
671
                            $ret[$matches[1]] .= $matches[2];
672
                        } else {
673
                            $ret[$matches[1]] = $matches[2];
674
                        }
675
                    }
676
677
                    // Now we have the header, so get the body from the rest of the lines.
678
                } else {
679
                    $body .= $line;
680
                }
681
            }
682
            // Finally we decode the message using yEnc.
683
            $ret['Message'] = ($yEnc ? Yenc::decodeIgnore($body) : $body);
684
        }
685
686
        return $ret;
687
    }
688
689
    /**
690
     * Download a full article header.
691
     *
692
     * @param string $groupName  The name of the group the article is in.
693
     * @param mixed  $identifier (string) The message-ID of the article to download.
694
     *                           (int)    The article number.
695
     *
696
     * @return mixed On success : (array)  The header.
697
     * @throws \Exception
698
     *               On failure : (object) PEAR_Error.
699
     */
700
    public function get_Header($groupName, $identifier)
701
    {
702
        $connected = $this->_checkConnection();
703
        if ($connected !== true) {
704
            return $connected;
705
        }
706
707
        // Make sure the requested group is already selected, if not select it.
708
        if (parent::group() !== $groupName) {
709
            // Select the group.
710
            $summary = $this->selectGroup($groupName);
711
            // Return PEAR error object on failure.
712
            if ($this->isError($summary)) {
713
                return $summary;
714
            }
715
        }
716
717
        // Check if it's an article number or message-id.
718
        if (! is_numeric($identifier)) {
719
            // Verify we have the required triangular brackets if it is a message-id.
720
            $identifier = $this->_formatMessageID($identifier);
721
        }
722
723
        // Download the header.
724
        $header = parent::getHeader($identifier);
725
        // If we failed, return PEAR error object.
726
        if ($this->isError($header)) {
727
            return $header;
728
        }
729
730
        $ret = $header;
731
        if (\count($header) > 0) {
0 ignored issues
show
Bug introduced by
It seems like $header can also be of type integer; however, parameter $var of count() does only seem to accept Countable|array, 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

731
        if (\count(/** @scrutinizer ignore-type */ $header) > 0) {
Loading history...
732
            $ret = [];
733
            // Use the line types of the header as array keys (From, Subject, etc).
734
            foreach ($header as $line) {
735
                if (preg_match('/([A-Z-]+?): (.*)/i', $line, $matches)) {
736
                    // If the line type takes more than 1 line, re-use the same array key.
737
                    if (array_key_exists($matches[1], $ret)) {
738
                        $ret[$matches[1]] .= $matches[2];
739
                    } else {
740
                        $ret[$matches[1]] = $matches[2];
741
                    }
742
                }
743
            }
744
        }
745
746
        return $ret;
747
    }
748
749
    /**
750
     * Post an article to usenet.
751
     *
752
     * @param string|array $groups   mixed   (array)  Groups. ie.: $groups = array('alt.test', 'alt.testing', 'free.pt');
753
     *                          (string) Group.  ie.: $groups = 'alt.test';
754
     * @param string $subject  string  The subject.     ie.: $subject = 'Test article';
755
     * @param string|\Exception $body     string  The message.     ie.: $message = 'This is only a test, please disregard.';
756
     * @param string $from     string  The poster.      ie.: $from = '<[email protected]>';
757
     * @param $extra    string  Extra, separated by \r\n
758
     *                                           ie.: $extra  = 'Organization: <NNTmux>\r\nNNTP-Posting-Host: <127.0.0.1>';
759
     * @param $yEnc     bool    Encode the message with yEnc?
760
     * @param $compress bool    Compress the message with GZip?
761
     *
762
     * @throws \Exception
763
     *
764
     * @return          mixed   On success : (bool)   True.
765
     *                          On failure : (object) PEAR_Error.
766
     */
767
    public function postArticle($groups, $subject, $body, $from, $yEnc = true, $compress = true, $extra = '')
768
    {
769
        if (! $this->_postingAllowed) {
770
            $message = 'You do not have the right to post articles on server '.$this->_currentServer;
771
772
            return $this->throwError(ColorCLI::error($message));
773
        }
774
775
        $connected = $this->_checkConnection();
776
        if ($connected !== true) {
777
            return $connected;
778
        }
779
780
        // Throw errors if subject or from are more than 510 chars.
781
        if (\strlen($subject) > 510) {
782
            $message = 'Max length of subject is 510 chars.';
783
784
            return $this->throwError(ColorCLI::error($message));
785
        }
786
787
        if (\strlen($from) > 510) {
788
            $message = 'Max length of from is 510 chars.';
789
790
            return $this->throwError(ColorCLI::error($message));
791
        }
792
793
        // Check if the group is string or array.
794
        if (\is_array($groups)) {
795
            $groups = implode(', ', $groups);
796
        }
797
798
        // Check if we should encode to yEnc.
799
        if ($yEnc) {
800
            $bin = $compress ? gzdeflate($body, 4) : $body;
801
            $body = Yenc::encode($bin, $subject);
802
        // If not yEnc, then check if the body is 510+ chars, split it at 510 chars and separate with \r\n
803
        } else {
804
            $body = $this->_splitLines($body, $compress);
805
        }
806
807
        // From is required by NNTP servers, but parent function mail does not require it, so format it.
808
        $from = 'From: '.$from;
809
        // If we had extra stuff to post, format it with from.
810
        if ($extra !== '') {
811
            $from = $from."\r\n".$extra;
812
        }
813
814
        return parent::mail($groups, $subject, $body, $from);
815
    }
816
817
    /**
818
     * Restart the NNTP connection if an error occurs in the selectGroup
819
     * function, if it does not restart display the error.
820
     *
821
     * @param NNTP   $nntp  Instance of class NNTP.
822
     * @param string $group Name of the group.
823
     * @param bool   $comp  Use compression or not?
824
     *
825
     * @return mixed On success : (array)  The group summary.
826
     * @throws \Exception
827
     *               On Failure : (object) PEAR_Error.
828
     */
829
    public function dataError($nntp, $group, $comp = true)
830
    {
831
        // Disconnect.
832
        $nntp->doQuit();
833
        // Try reconnecting. This uses another round of max retries.
834
        if ($nntp->doConnect($comp) !== true) {
835
            return $this->throwError('Unable to reconnect to usenet!');
836
        }
837
838
        // Try re-selecting the group.
839
        $data = $nntp->selectGroup($group);
840
        if ($this->isError($data)) {
841
            $message = "Code {$data->code}: {$data->message}\nSkipping group: {$group}";
842
843
            if ($this->_echo) {
844
                ColorCLI::doEcho(ColorCLI::error($message), true);
845
            }
846
            $nntp->doQuit();
847
        }
848
849
        return $data;
850
    }
851
852
    /**
853
     * Path to yyDecoder binary.
854
     * @var bool|string
855
     */
856
    protected $_yyDecoderPath;
857
858
    /**
859
     * If on unix, hide yydecode CLI output.
860
     * @var string
861
     */
862
    protected $_yEncSilence;
863
864
    /**
865
     * Path to temp yEnc input storage file.
866
     * @var string
867
     */
868
    protected $_yEncTempInput;
869
870
    /**
871
     * Path to temp yEnc output storage file.
872
     * @var string
873
     */
874
    protected $_yEncTempOutput;
875
876
    /**
877
     * Split a string into lines of 510 chars ending with \r\n.
878
     * Usenet limits lines to 512 chars, with \r\n that leaves us 510.
879
     *
880
     * @param string $string   The string to split.
881
     * @param bool   $compress Compress the string with gzip?
882
     *
883
     * @return string The split string.
884
     */
885
    protected function _splitLines($string, $compress = false): string
886
    {
887
        // Check if the length is longer than 510 chars.
888
        if (\strlen($string) > 510) {
889
            // If it is, split it @ 510 and terminate with \r\n.
890
            $string = chunk_split($string, 510, "\r\n");
891
        }
892
893
        // Compress the string if requested.
894
        return $compress ? gzdeflate($string, 4) : $string;
895
    }
896
897
    /**
898
     * Try to see if the NNTP server implements XFeature GZip Compression,
899
     * change the compression bool object if so.
900
     *
901
     * @param bool $secondTry This is only used if enabling compression fails, the function will call itself to retry.
902
     * @return mixed On success : (bool)   True:  The server understood and compression is enabled.
903
     *                            (bool)   False: The server did not understand, compression is not enabled.
904
     *               On failure : (object) PEAR_Error.
905
     * @throws \Exception
906
     */
907
    protected function _enableCompression($secondTry = false)
908
    {
909
        if ($this->_compressionEnabled === true) {
910
            return true;
911
        }
912
        if ($this->_compressionSupported === false) {
913
            return false;
914
        }
915
916
        // Send this command to the usenet server.
917
        $response = $this->_sendCommand('XFEATURE COMPRESS GZIP');
918
919
        // Check if it's good.
920
        if ($this->isError($response)) {
921
            $this->_compressionSupported = false;
922
923
            return $response;
924
        }
925
        if ($response !== 290) {
926
            if ($secondTry === false) {
927
                // Retry.
928
                $this->cmdQuit();
929
                if ($this->_checkConnection()) {
930
                    return $this->_enableCompression(true);
931
                }
932
            }
933
            $msg = "Sent 'XFEATURE COMPRESS GZIP' to server, got '$response: ".$this->_currentStatusResponse()."'";
0 ignored issues
show
Unused Code introduced by
The assignment to $msg is dead and can be removed.
Loading history...
934
935
            $this->_compressionSupported = false;
936
937
            return false;
938
        }
939
940
        $this->_compressionEnabled = true;
941
        $this->_compressionSupported = true;
942
943
        return true;
944
    }
945
946
    /**
947
     * Override PEAR NNTP's function to use our _getXFeatureTextResponse instead
948
     * of their _getTextResponse function since it is incompatible at decoding
949
     * headers when XFeature GZip compression is enabled server side.
950
     *
951
     * @return self|string    Our overridden function when compression is enabled.
952
     *         parent  Parent function when no compression.
953
     */
954
    protected function _getTextResponse()
955
    {
956
        if ($this->_compressionEnabled === true &&
957
            isset($this->_currentStatusResponse[1]) &&
958
            stripos($this->_currentStatusResponse[1], 'COMPRESS=GZIP') !== false) {
959
            return $this->_getXFeatureTextResponse();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_getXFeatureTextResponse() returns the type array which is incompatible with the documented return type string|Blacklight\NNTP.
Loading history...
960
        }
961
962
        return parent::_getTextResponse();
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::_getTextResponse() also could return the type array|string[] which is incompatible with the documented return type string|Blacklight\NNTP.
Loading history...
963
    }
964
965
    /**
966
     * Loop over the compressed data when XFeature GZip Compress is turned on,
967
     * string the data until we find a indicator
968
     * (period, carriage feed, line return ;; .\r\n), decompress the data,
969
     * split the data (bunch of headers in a string) into an array, finally
970
     * return the array.
971
     *
972
     * Have we failed to decompress the data, was there a
973
     * problem downloading the data, etc..
974
     * @return array|string  On success : (array)  The headers.
975
     *                       On failure : (object) PEAR_Error.
976
     *                       On decompress failure: (string) error message
977
     */
978
    protected function &_getXFeatureTextResponse()
979
    {
980
        $possibleTerm = false;
981
        $data = null;
982
983
        while (! feof($this->_socket)) {
984
985
            // Did we find a possible ending ? (.\r\n)
986
            if ($possibleTerm !== false) {
987
988
                // Loop, sleeping shortly, to allow the server time to upload data, if it has any.
989
                for ($i = 0; $i < 3; $i++) {
990
                    // If the socket is really empty, fGets will get stuck here, so set the socket to non blocking in case.
991
                    stream_set_blocking($this->_socket, 0);
992
993
                    // Now try to download from the socket.
994
                    $buffer = fgets($this->_socket);
995
996
                    // And set back the socket to blocking.
997
                    stream_set_blocking($this->_socket, 1);
998
999
                    // Don't sleep on last iteration.
1000
                    if (! empty($buffer)) {
1001
                        break;
1002
                    }
1003
                    if ($i < 2) {
1004
                        usleep(10000);
1005
                    }
1006
                }
1007
1008
                // If the buffer was really empty, then we know $possibleTerm was the real ending.
1009
                if (empty($buffer)) {
1010
                    // Remove .\r\n from end, decompress data.
1011
                    $deComp = @gzuncompress(mb_substr($data, 0, -3, '8bit'));
1012
1013
                    if (! empty($deComp)) {
1014
                        $bytesReceived = \strlen($data);
1015
                        if ($this->_echo && $bytesReceived > 10240) {
1016
                            ColorCLI::doEcho(
1017
                                ColorCLI::primaryOver(
1018
                                    'Received '.round($bytesReceived / 1024).
1019
                                    'KB from group ('.$this->group().').'
1020
                                ),
1021
                                true
1022
                            );
1023
                        }
1024
1025
                        // Split the string of headers into an array of individual headers, then return it.
1026
                        $deComp = explode("\r\n", trim($deComp));
1027
1028
                        return $deComp;
1029
                    }
1030
                    $message = 'Decompression of OVER headers failed.';
1031
1032
                    $message = $this->throwError(ColorCLI::error($message), 1000);
1033
1034
                    return $message;
1035
                }
1036
                // The buffer was not empty, so we know this was not the real ending, so reset $possibleTerm.
1037
                $possibleTerm = false;
1038
            } else {
1039
                // Get data from the stream.
1040
                $buffer = fgets($this->_socket);
1041
            }
1042
1043
            // If we got no data at all try one more time to pull data.
1044
            if (empty($buffer)) {
1045
                usleep(10000);
1046
                $buffer = fgets($this->_socket);
1047
1048
                // If wet got nothing again, return error.
1049
                if (empty($buffer)) {
1050
                    $message = 'Error fetching data from usenet server while downloading OVER headers.';
1051
1052
                    $message = $this->throwError(ColorCLI::error($message), 1000);
1053
1054
                    return $message;
1055
                }
1056
            }
1057
1058
            // Append current buffer to rest of buffer.
1059
            $data .= $buffer;
1060
1061
            // Check if we have the ending (.\r\n)
1062
            if (substr($buffer, -3) === ".\r\n") {
1063
                // We have a possible ending, next loop check if it is.
1064
                $possibleTerm = true;
1065
            }
1066
        }
1067
1068
        $message = 'Unspecified error while downloading OVER headers.';
1069
        $message = $this->throwError(ColorCLI::error($message), 1000);
1070
1071
        return $message;
1072
    }
1073
1074
    /**
1075
     * Check if the Message-ID has the required opening and closing brackets.
1076
     *
1077
     * @param  string $messageID The Message-ID with or without brackets.
1078
     *
1079
     * @return string            Message-ID with brackets.
1080
     */
1081
    protected function _formatMessageID($messageID): string
1082
    {
1083
        $messageID = (string) $messageID;
1084
        if ($messageID === '') {
1085
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return string.
Loading history...
1086
        }
1087
1088
        // Check if the first char is <, if not add it.
1089
        if ($messageID[0] !== '<') {
1090
            $messageID = ('<'.$messageID);
1091
        }
1092
1093
        // Check if the last char is >, if not add it.
1094
        if (substr($messageID, -1) !== '>') {
1095
            $messageID .= '>';
1096
        }
1097
1098
        return $messageID;
1099
    }
1100
1101
    /**
1102
     * Download an article body (an article without the header).
1103
     *
1104
     * @param string $groupName  The name of the group the article is in.
1105
     * @param mixed  $identifier (string) The message-ID of the article to download.
1106
     *                           (int)    The article number.
1107
     *
1108
     * @return string On success : (string) The article's body.
1109
     * @throws \Exception
1110
     *               On failure : (object) PEAR_Error.
1111
     */
1112
    protected function _getMessage($groupName, $identifier): ?string
1113
    {
1114
        // Make sure the requested group is already selected, if not select it.
1115
        if (parent::group() !== $groupName) {
1116
            // Select the group.
1117
            $summary = $this->selectGroup($groupName);
1118
            // If there was an error selecting the group, return PEAR error object.
1119
            if ($this->isError($summary)) {
1120
                return $summary;
1121
            }
1122
        }
1123
1124
        // Check if this is an article number or message-id.
1125
        if (! is_numeric($identifier)) {
1126
            // It's a message-id so check if it has the triangular brackets.
1127
            $identifier = $this->_formatMessageID($identifier);
1128
        }
1129
1130
        // Tell the news server we want the body of an article.
1131
        $response = $this->_sendCommand('BODY '.$identifier);
1132
        if ($this->isError($response)) {
1133
            return $response;
1134
        }
1135
1136
        $body = '';
1137
        if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) {
0 ignored issues
show
Bug introduced by
The constant Blacklight\NET_NNTP_PROT...SPONSECODE_BODY_FOLLOWS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1138
1139
            // Continue until connection is lost
1140
            while (! feof($this->_socket)) {
1141
1142
                // Retrieve and append up to 1024 characters from the server.
1143
                $line = fgets($this->_socket, 1024);
1144
1145
                // If the socket is empty/ an error occurs, false is returned.
1146
                // Since the socket is blocking, the socket should not be empty, so it's definitely an error.
1147
                if ($line === false) {
1148
                    return $this->throwError('Failed to read line from socket.', null);
1149
                }
1150
1151
                // Check if the line terminates the text response.
1152
                if ($line === ".\r\n") {
1153
1154
                    // Attempt to yEnc decode and return the body.
1155
                    return Yenc::decodeIgnore($body);
1156
                }
1157
1158
                // Check for line that starts with double period, remove one.
1159
                if ($line[0] === '.' && $line[1] === '.') {
1160
                    $line = substr($line, 1);
1161
                }
1162
1163
                // Add the line to the rest of the lines.
1164
                $body .= $line;
1165
            }
1166
1167
            return $this->throwError('End of stream! Connection lost?', null);
1168
        }
1169
1170
        return $this->_handleErrorResponse($response);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_handleErrorResponse($response) returns the type object which is incompatible with the type-hinted return null|string.
Loading history...
1171
    }
1172
1173
    /**
1174
     * Check if we are still connected. Reconnect if not.
1175
     *
1176
     * @param  bool $reSelectGroup Select back the group after connecting?
1177
     *
1178
     * @return mixed On success: (bool)   True;
1179
     * @throws \Exception
1180
     *               On failure: (object) PEAR_Error
1181
     */
1182
    protected function _checkConnection($reSelectGroup = true)
1183
    {
1184
        $currentGroup = $this->_currentGroup;
1185
        // Check if we are connected.
1186
        if (parent::_isConnected()) {
1187
            $retVal = true;
1188
        } else {
1189
            switch ($this->_currentServer) {
1190
                case env('NNTP_SERVER'):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1191
                    if (\is_resource($this->_socket)) {
1192
                        $this->doQuit(true);
1193
                    }
1194
                    $retVal = $this->doConnect();
1195
                    break;
1196
                case env('NNTP_SERVER_A'):
1197
                    if (\is_resource($this->_socket)) {
1198
                        $this->doQuit(true);
1199
                    }
1200
                    $retVal = $this->doConnect(true, true);
1201
                    break;
1202
                default:
1203
                    $retVal = $this->throwError('Wrong server constant used in NNTP checkConnection()!');
1204
            }
1205
1206
            if ($retVal === true && $reSelectGroup) {
1207
                $group = $this->selectGroup($currentGroup);
1208
                if ($this->isError($group)) {
1209
                    $retVal = $group;
1210
                }
1211
            }
1212
        }
1213
1214
        return $retVal;
1215
    }
1216
}
1217