Completed
Push — dev ( 1b04e6...9c94ea )
by Darko
07:39
created

NNTP   F

Complexity

Total Complexity 216

Size/Duplication

Total Lines 1369
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 216
eloc 513
dl 0
loc 1369
ccs 0
cts 403
cp 0
rs 2
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getOverview() 0 11 2
A __construct() 0 14 3
D _handleErrorResponse() 0 70 22
C get_Article() 0 64 13
D getMessages() 0 97 21
C getXOVER() 0 71 12
B _enableCompression() 0 37 7
A selectGroup() 0 15 5
A _getTextResponse() 0 9 4
F connect() 0 79 18
A getGroups() 0 6 1
A _isConnected() 0 3 3
B _checkConnection() 0 33 9
A dataError() 0 21 4
A __destruct() 0 3 1
A enableCompression() 0 6 2
A doQuit() 0 11 3
B postArticle() 0 48 9
A _splitLines() 0 10 3
A _formatMessageID() 0 18 4
A _resetProperties() 0 8 1
B _getMessage() 0 59 11
F doConnect() 0 136 35
B get_Header() 0 47 10
C _getXFeatureTextResponse() 0 91 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 App\Models\Settings;
6
use App\Extensions\util\PhpYenc;
7
use Blacklight\utility\Utility;
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
 *
15
 * Class NNTP
16
 */
17
18
/*
19
* 'Service discontinued' (RFC977)
20
*
21
* @access     public
22
*/
23
define('NET_NNTP_PROTOCOL_RESPONSECODE_DISCONNECTING_FORCED', 400);
24
25
/*
26
 * 'Groups and descriptions unavailable'
27
 *
28
 * @access     public
29
 */
30
define('NET_NNTP_PROTOCOL_RESPONSECODE_XGTITLE_GROUPS_UNAVAILABLE', 481);
31
32
/*
33
 * 'Can not initiate TLS negotiation' (RFC4642)
34
 *
35
 * @access     public
36
 */
37
define('NET_NNTP_PROTOCOL_RESPONSECODE_TLS_FAILED_NEGOTIATION', 580);
38
39
class NNTP extends \Net_NNTP_Client
40
{
41
    /**
42
     * @var \Blacklight\ColorCLI
43
     */
44
    protected $colorCli;
45
46
    /**
47
     * @var bool
48
     */
49
    protected $_debugBool;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $_echo;
55
56
    /**
57
     * Does the server support XFeature GZip header compression?
58
     * @var bool
59
     */
60
    protected $_compressionSupported = true;
61
62
    /**
63
     * Is header compression enabled for the session?
64
     * @var bool
65
     */
66
    protected $_compressionEnabled = false;
67
68
    /**
69
     * Currently selected group.
70
     * @var string
71
     */
72
    protected $_currentGroup = '';
73
74
    /**
75
     * Port of the current NNTP server.
76
     * @var int
77
     */
78
    protected $_currentPort = 'NNTP_PORT';
79
80
    /**
81
     * Address of the current NNTP server.
82
     * @var string
83
     */
84
    protected $_currentServer = 'NNTP_SERVER';
85
86
    /**
87
     * Are we allowed to post to usenet?
88
     * @var bool
89
     */
90
    protected $_postingAllowed = false;
91
92
    /**
93
     * How many times should we try to reconnect to the NNTP server?
94
     * @var int
95
     */
96
    protected $_nntpRetries;
97
98
    /**
99
     * Seconds to wait for the blocking socket to timeout.
100
     *
101
     * @var int
102
     */
103
    protected $_socketTimeout = 120;
104
105
    /**
106
     * Default constructor.
107
     *
108
     * @param array $options Class instances and echo to CLI bool.
109
     *
110
     * @throws \Exception
111
     */
112
    public function __construct(array $options = [])
113
    {
114
        $defaults = [
115
            'Echo'      => true,
116
            'Logger' => null,
117
        ];
118
        $options += $defaults;
119
120
        parent::__construct();
121
122
        $this->_echo = ($options['Echo'] && config('nntmux.echocli'));
123
124
        $this->_nntpRetries = Settings::settingValue('..nntpretries') !== '' ? (int) Settings::settingValue('..nntpretries') : 0 + 1;
125
        $this->colorCli = new ColorCLI();
126
    }
127
128
    /**
129
     * Destruct.
130
     * Close the NNTP connection if still connected.
131
     */
132
    public function __destruct()
133
    {
134
        $this->doQuit();
135
    }
136
137
    /**
138
     * Connect to a usenet server.
139
     *
140
     * @param bool $compression Should we attempt to enable XFeature Gzip compression on this connection?
141
     * @param bool $alternate   Use the alternate NNTP connection.
142
     *
143
     * @return mixed  On success = (bool)   Did we successfully connect to the usenet?
144
     * @throws \Exception
145
     *                On failure = (object) PEAR_Error.
146
     */
147
    public function doConnect($compression = true, $alternate = false)
148
    {
149
        if ($this->_isConnected() && (($alternate && $this->_currentServer === env('NNTP_SERVER_A')) || (! $alternate && $this->_currentServer === env('NNTP_SERVER')))) {
150
            return true;
151
        }
152
153
        $this->doQuit();
154
155
        $ret = $connected = $cError = $aError = false;
156
157
        // Set variables to connect based on if we are using the alternate provider or not.
158
        if (! $alternate) {
159
            $sslEnabled = env('NNTP_SSLENABLED') ? true : false;
160
            $this->_currentServer = env('NNTP_SERVER');
0 ignored issues
show
Documentation Bug introduced by
It seems like env('NNTP_SERVER') can also be of type boolean. However, the property $_currentServer is declared as type string. 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...
161
            $this->_currentPort = env('NNTP_PORT');
0 ignored issues
show
Documentation Bug introduced by
It seems like env('NNTP_PORT') can also be of type boolean or string. However, the property $_currentPort 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...
162
            $userName = env('NNTP_USERNAME');
163
            $password = env('NNTP_PASSWORD');
164
            $socketTimeout = ! empty(env('NNTP_SOCKET_TIMEOUT')) ? env('NNTP_SOCKET_TIMEOUT') : $this->_socketTimeout;
165
        } else {
166
            $sslEnabled = env('NNTP_SSLENABLED_A') ? true : false;
167
            $this->_currentServer = env('NNTP_SERVER_A');
168
            $this->_currentPort = env('NNTP_PORT_A');
169
            $userName = env('NNTP_USERNAME_A');
170
            $password = env('NNTP_PASSWORD_A');
171
            $socketTimeout = ! empty(env('NNTP_SOCKET_TIMEOUT_A')) ? env('NNTP_SOCKET_TIMEOUT_A') : $this->_socketTimeout;
172
        }
173
174
        $enc = ($sslEnabled ? ' (ssl)' : ' (non-ssl)');
175
        $sslEnabled = ($sslEnabled ? 'tls' : false);
176
177
        // Try to connect until we run of out tries.
178
        $retries = $this->_nntpRetries;
179
        while (true) {
180
            $retries--;
181
            $authenticated = false;
182
183
            // If we are not connected, try to connect.
184
            if (! $connected) {
185
                $ret = $this->connect($this->_currentServer, $sslEnabled, $this->_currentPort, 5, $socketTimeout);
0 ignored issues
show
Bug introduced by
It seems like $this->_currentPort can also be of type boolean and string; however, parameter $port of Blacklight\NNTP::connect() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

185
                $ret = $this->connect($this->_currentServer, $sslEnabled, /** @scrutinizer ignore-type */ $this->_currentPort, 5, $socketTimeout);
Loading history...
Bug introduced by
It seems like $socketTimeout can also be of type boolean and string; however, parameter $socketTimeout of Blacklight\NNTP::connect() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

185
                $ret = $this->connect($this->_currentServer, $sslEnabled, $this->_currentPort, 5, /** @scrutinizer ignore-type */ $socketTimeout);
Loading history...
186
            }
187
188
            // Check if we got an error while connecting.
189
            $cErr = self::isError($ret);
190
191
            // If no error, we are connected.
192
            if (! $cErr) {
193
                // Say that we are connected so we don't retry.
194
                $connected = true;
195
                // When there is no error it returns bool if we are allowed to post or not.
196
                $this->_postingAllowed = $ret;
197
            } else {
198
                // Only fetch the message once.
199
                if (! $cError) {
200
                    $cError = $ret->getMessage();
201
                }
202
            }
203
204
            // If error, try to connect again.
205
            if ($cErr && $retries > 0) {
206
                continue;
207
            }
208
209
            // If we have no more retries and could not connect, return an error.
210
            if ($retries === 0 && ! $connected) {
211
                $message =
212
                    'Cannot connect to server '.
213
                    $this->_currentServer.
214
                    $enc.
215
                    ': '.
216
                    $cError;
217
218
                return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
The method throwError() does not exist on Blacklight\NNTP. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

218
                return $this->/** @scrutinizer ignore-call */ throwError($this->colorCli->error($message));
Loading history...
219
            }
220
221
            // If we are connected, try to authenticate.
222
            if ($connected && ! $authenticated) {
223
224
                // If the username is empty it probably means the server does not require a username.
225
                if ($userName === '') {
226
                    $authenticated = true;
227
228
                // Try to authenticate to usenet.
229
                } else {
230
                    $ret2 = $this->authenticate($userName, $password);
231
232
                    // Check if there was an error authenticating.
233
                    $aErr = self::isError($ret2);
234
235
                    // If there was no error, then we are authenticated.
236
                    if (! $aErr) {
237
                        $authenticated = true;
238
                    } elseif (! $aError) {
239
                        $aError = $ret2->getMessage();
240
                    }
241
242
                    // If error, try to authenticate again.
243
                    if ($aErr && $retries > 0) {
244
                        continue;
245
                    }
246
247
                    // If we ran out of retries, return an error.
248
                    if ($retries === 0 && ! $authenticated) {
249
                        $message =
250
                            'Cannot authenticate to server '.
251
                            $this->_currentServer.
252
                            $enc.
253
                            ' - '.
254
                            $userName.
255
                            ' ('.$aError.')';
256
257
                        return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
258
                    }
259
                }
260
            }
261
262
            // If we are connected and authenticated, try enabling compression if we have it enabled.
263
            if ($connected && $authenticated) {
264
                // Check if we should use compression on the connection.
265
                if (! $compression || (int) Settings::settingValue('..compressedheaders') === 0) {
266
                    $this->_compressionSupported = false;
267
                }
268
269
                return true;
270
            }
271
            // If we reached this point and have not connected after all retries, break out of the loop.
272
            if ($retries === 0) {
273
                break;
274
            }
275
276
            // Sleep .4 seconds between retries.
277
            usleep(400000);
278
        }
279
        // If we somehow got out of the loop, return an error.
280
        $message = 'Unable to connect to '.$this->_currentServer.$enc;
281
282
        return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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

564
                        $aConnected = $this->_currentServer === env('NNTP_SERVER') ? $nntp->/** @scrutinizer ignore-call */ doConnect(true, true) : $nntp->doConnect();

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...
565
                    }
566
                    // If we connected successfully to usenet try to download the article body.
567
                    if ($aConnected === true) {
568
                        $newBody = $nntp->_getMessage($groupName, $wanted);
569
                        // Check if we got an error.
570
                        if ($nntp->isError($newBody)) {
571
                            if ($aConnected) {
572
                                $nntp->doQuit();
573
                            }
574
                            // If we got some data, return it.
575
                            if ($body !== '') {
576
                                return $body;
577
                            }
578
                            // Return the error.
579
                            return $newBody;
580
                        }
581
                        // Append the alternate body to the main body.
582
                        $body .= $newBody;
583
                    }
584
                } else {
585
                    // If we got some data, return it.
586
                    if ($body !== '') {
587
                        return $body;
588
                    }
589
590
                    return $message;
591
                }
592
            }
593
594
            // If it's a string check if it's a valid message-ID.
595
        } elseif (\is_string($identifiers) || is_numeric($identifiers)) {
596
            $body = $this->_getMessage($groupName, $identifiers);
597
            if ($alternate && self::isError($body)) {
598
                $nntp->doConnect(true, true);
599
                $body = $nntp->_getMessage($groupName, $identifiers);
600
                $aConnected = true;
601
            }
602
603
            // Else return an error.
604
        } else {
605
            $message = 'Wrong Identifier type, array, int or string accepted. This type of var was passed: '.gettype($identifiers);
606
607
            return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
608
        }
609
610
        if ($aConnected === true) {
611
            $nntp->doQuit();
612
        }
613
614
        return $body;
615
    }
616
617
    /**
618
     * Download a full article, the body and the header, return an array with named keys and their
619
     * associated values, optionally decode the body using yEnc.
620
     *
621
     * @param string $groupName The name of the group the article is in.
622
     * @param mixed $identifier (string)The message-ID of the article to download.
623
     *                           (int) The article number.
624
     * @param bool $yEnc Attempt to yEnc decode the body.
625
     *
626
     * @return mixed  On success : (array)  The article.
627
     *                On failure : (object) PEAR_Error.
628
     * @throws \Exception
629
     */
630
    public function get_Article($groupName, $identifier, $yEnc = false)
631
    {
632
        $connected = $this->_checkConnection();
633
        if ($connected !== true) {
634
            return $connected;
635
        }
636
637
        // Make sure the requested group is already selected, if not select it.
638
        if ($this->group() !== $groupName) {
639
            // Select the group.
640
            $summary = $this->selectGroup($groupName);
641
            // If there was an error selecting the group, return PEAR error object.
642
            if (self::isError($summary)) {
643
                return $summary;
644
            }
645
        }
646
647
        // Check if it's an article number or message-ID.
648
        if (! is_numeric($identifier)) {
649
            // If it's a message-ID, check if it has the required triangular brackets.
650
            $identifier = $this->_formatMessageID($identifier);
651
        }
652
653
        // Download the article.
654
        $article = $this->getArticle($identifier);
655
        // If there was an error downloading the article, return a PEAR error object.
656
        if (self::isError($article)) {
657
            return $article;
658
        }
659
660
        $ret = $article;
661
        // Make sure the article is an array and has more than 1 element.
662
        if (\count($article) > 0) {
663
            $ret = [];
664
            $body = '';
665
            $emptyLine = false;
666
            foreach ($article as $line) {
667
                // If we found the empty line it means we are done reading the header and we will start reading the body.
668
                if (! $emptyLine) {
669
                    if ($line === '') {
670
                        $emptyLine = true;
671
                        continue;
672
                    }
673
674
                    // Use the line type of the article as the array key (From, Subject, etc..).
675
                    if (preg_match('/([A-Z-]+?): (.*)/i', $line, $matches)) {
676
                        // If the line type takes more than 1 line, append the rest of the content to the same key.
677
                        if (array_key_exists($matches[1], $ret)) {
678
                            $ret[$matches[1]] .= $matches[2];
679
                        } else {
680
                            $ret[$matches[1]] = $matches[2];
681
                        }
682
                    }
683
684
                    // Now we have the header, so get the body from the rest of the lines.
685
                } else {
686
                    $body .= $line;
687
                }
688
            }
689
            // Finally we decode the message using yEnc.
690
            $ret['Message'] = ($yEnc ? PhpYenc::decodeIgnore($body) : $body);
691
        }
692
693
        return $ret;
694
    }
695
696
    /**
697
     * Download a full article header.
698
     *
699
     * @param string $groupName  The name of the group the article is in.
700
     * @param mixed  $identifier (string) The message-ID of the article to download.
701
     *                           (int)    The article number.
702
     *
703
     * @return mixed On success : (array)  The header.
704
     * @throws \Exception
705
     *               On failure : (object) PEAR_Error.
706
     */
707
    public function get_Header($groupName, $identifier)
708
    {
709
        $connected = $this->_checkConnection();
710
        if ($connected !== true) {
711
            return $connected;
712
        }
713
714
        // Make sure the requested group is already selected, if not select it.
715
        if ($this->group() !== $groupName) {
716
            // Select the group.
717
            $summary = $this->selectGroup($groupName);
718
            // Return PEAR error object on failure.
719
            if (self::isError($summary)) {
720
                return $summary;
721
            }
722
        }
723
724
        // Check if it's an article number or message-id.
725
        if (! is_numeric($identifier)) {
726
            // Verify we have the required triangular brackets if it is a message-id.
727
            $identifier = $this->_formatMessageID($identifier);
728
        }
729
730
        // Download the header.
731
        $header = $this->getHeader($identifier);
732
        // If we failed, return PEAR error object.
733
        if (self::isError($header)) {
734
            return $header;
735
        }
736
737
        $ret = $header;
738
        if (\count($header) > 0) {
739
            $ret = [];
740
            // Use the line types of the header as array keys (From, Subject, etc).
741
            foreach ($header as $line) {
742
                if (preg_match('/([A-Z-]+?): (.*)/i', $line, $matches)) {
743
                    // If the line type takes more than 1 line, re-use the same array key.
744
                    if (array_key_exists($matches[1], $ret)) {
745
                        $ret[$matches[1]] .= $matches[2];
746
                    } else {
747
                        $ret[$matches[1]] = $matches[2];
748
                    }
749
                }
750
            }
751
        }
752
753
        return $ret;
754
    }
755
756
    /**
757
     * Post an article to usenet.
758
     *
759
     * @param string|array $groups   mixed   (array)  Groups. ie.: $groups = array('alt.test', 'alt.testing', 'free.pt');
760
     *                          (string) Group.  ie.: $groups = 'alt.test';
761
     * @param string $subject  string  The subject.     ie.: $subject = 'Test article';
762
     * @param string|\Exception $body     string  The message.     ie.: $message = 'This is only a test, please disregard.';
763
     * @param string $from     string  The poster.      ie.: $from = '<[email protected]>';
764
     * @param $extra    string  Extra, separated by \r\n
765
     *                                           ie.: $extra  = 'Organization: <NNTmux>\r\nNNTP-Posting-Host: <127.0.0.1>';
766
     * @param $yEnc     bool    Encode the message with yEnc?
767
     * @param $compress bool    Compress the message with GZip?
768
     *
769
     * @throws \Exception
770
     *
771
     * @return          mixed   On success : (bool)   True.
772
     *                          On failure : (object) PEAR_Error.
773
     */
774
    public function postArticle($groups, $subject, $body, $from, $yEnc = true, $compress = true, $extra = '')
775
    {
776
        if (! $this->_postingAllowed) {
777
            $message = 'You do not have the right to post articles on server '.$this->_currentServer;
778
779
            return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
780
        }
781
782
        $connected = $this->_checkConnection();
783
        if ($connected !== true) {
784
            return $connected;
785
        }
786
787
        // Throw errors if subject or from are more than 510 chars.
788
        if (\strlen($subject) > 510) {
789
            $message = 'Max length of subject is 510 chars.';
790
791
            return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
792
        }
793
794
        if (\strlen($from) > 510) {
795
            $message = 'Max length of from is 510 chars.';
796
797
            return $this->throwError($this->colorCli->error($message));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
798
        }
799
800
        // Check if the group is string or array.
801
        if (\is_array($groups)) {
802
            $groups = implode(', ', $groups);
803
        }
804
805
        // Check if we should encode to yEnc.
806
        if ($yEnc) {
807
            $bin = $compress ? gzdeflate($body, 4) : $body;
808
            $body = PhpYenc::encode($bin, $subject);
809
        // If not yEnc, then check if the body is 510+ chars, split it at 510 chars and separate with \r\n
810
        } else {
811
            $body = $this->_splitLines($body, $compress);
812
        }
813
814
        // From is required by NNTP servers, but parent function mail does not require it, so format it.
815
        $from = 'From: '.$from;
816
        // If we had extra stuff to post, format it with from.
817
        if ($extra !== '') {
818
            $from = $from."\r\n".$extra;
819
        }
820
821
        return $this->mail($groups, $subject, $body, $from);
822
    }
823
824
    /**
825
     * Restart the NNTP connection if an error occurs in the selectGroup
826
     * function, if it does not restart display the error.
827
     *
828
     * @param NNTP   $nntp  Instance of class NNTP.
829
     * @param string $group Name of the group.
830
     * @param bool   $comp  Use compression or not?
831
     *
832
     * @return mixed On success : (array)  The group summary.
833
     * @throws \Exception
834
     *               On Failure : (object) PEAR_Error.
835
     */
836
    public function dataError($nntp, $group, $comp = true)
837
    {
838
        // Disconnect.
839
        $nntp->doQuit();
840
        // Try reconnecting. This uses another round of max retries.
841
        if ($nntp->doConnect($comp) !== true) {
842
            return $this->throwError('Unable to reconnect to usenet!');
843
        }
844
845
        // Try re-selecting the group.
846
        $data = $nntp->selectGroup($group);
847
        if (self::isError($data)) {
848
            $message = "Code {$data->code}: {$data->message}\nSkipping group: {$group}";
849
850
            if ($this->_echo) {
851
                $this->colorCli->error($message);
852
            }
853
            $nntp->doQuit();
854
        }
855
856
        return $data;
857
    }
858
859
    /**
860
     * Path to yyDecoder binary.
861
     * @var bool|string
862
     */
863
    protected $_yyDecoderPath;
864
865
    /**
866
     * If on unix, hide yydecode CLI output.
867
     * @var string
868
     */
869
    protected $_yEncSilence;
870
871
    /**
872
     * Path to temp yEnc input storage file.
873
     * @var string
874
     */
875
    protected $_yEncTempInput;
876
877
    /**
878
     * Path to temp yEnc output storage file.
879
     * @var string
880
     */
881
    protected $_yEncTempOutput;
882
883
    /**
884
     * Split a string into lines of 510 chars ending with \r\n.
885
     * Usenet limits lines to 512 chars, with \r\n that leaves us 510.
886
     *
887
     * @param string $string   The string to split.
888
     * @param bool   $compress Compress the string with gzip?
889
     *
890
     * @return string The split string.
891
     */
892
    protected function _splitLines($string, $compress = false): string
893
    {
894
        // Check if the length is longer than 510 chars.
895
        if (\strlen($string) > 510) {
896
            // If it is, split it @ 510 and terminate with \r\n.
897
            $string = chunk_split($string, 510, "\r\n");
898
        }
899
900
        // Compress the string if requested.
901
        return $compress ? gzdeflate($string, 4) : $string;
902
    }
903
904
    /**
905
     * Try to see if the NNTP server implements XFeature GZip Compression,
906
     * change the compression bool object if so.
907
     *
908
     * @param bool $secondTry This is only used if enabling compression fails, the function will call itself to retry.
909
     * @return mixed On success : (bool)   True:  The server understood and compression is enabled.
910
     *                            (bool)   False: The server did not understand, compression is not enabled.
911
     *               On failure : (object) PEAR_Error.
912
     * @throws \Exception
913
     */
914
    protected function _enableCompression($secondTry = false)
915
    {
916
        if ($this->_compressionEnabled) {
917
            return true;
918
        }
919
        if (! $this->_compressionSupported) {
920
            return false;
921
        }
922
923
        // Send this command to the usenet server.
924
        $response = $this->_sendCommand('XFEATURE COMPRESS GZIP');
925
926
        // Check if it's good.
927
        if (self::isError($response)) {
928
            $this->_compressionSupported = false;
929
930
            return $response;
931
        }
932
        if ($response !== 290) {
933
            if (! $secondTry) {
934
                // Retry.
935
                $this->cmdQuit();
936
                if ($this->_checkConnection()) {
937
                    return $this->_enableCompression(true);
938
                }
939
            }
940
            $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...
941
942
            $this->_compressionSupported = false;
943
944
            return false;
945
        }
946
947
        $this->_compressionEnabled = true;
948
        $this->_compressionSupported = true;
949
950
        return true;
951
    }
952
953
    /**
954
     * Override PEAR NNTP's function to use our _getXFeatureTextResponse instead
955
     * of their _getTextResponse function since it is incompatible at decoding
956
     * headers when XFeature GZip compression is enabled server side.
957
     *
958
     * @return self|string    Our overridden function when compression is enabled.
959
     *         parent  Parent function when no compression.
960
     */
961
    public function _getTextResponse()
962
    {
963
        if ($this->_compressionEnabled &&
964
            isset($this->_currentStatusResponse[1]) &&
965
            stripos($this->_currentStatusResponse[1], 'COMPRESS=GZIP') !== false) {
966
            return $this->_getXFeatureTextResponse();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_getXFeatureTextResponse() also could return the type array which is incompatible with the documented return type Blacklight\NNTP|string.
Loading history...
967
        }
968
969
        return parent::_getTextResponse();
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::_getTextResponse() also could return the type array which is incompatible with the documented return type Blacklight\NNTP|string.
Loading history...
970
    }
971
972
    /**
973
     * Loop over the compressed data when XFeature GZip Compress is turned on,
974
     * string the data until we find a indicator
975
     * (period, carriage feed, line return ;; .\r\n), decompress the data,
976
     * split the data (bunch of headers in a string) into an array, finally
977
     * return the array.
978
     *
979
     * Have we failed to decompress the data, was there a
980
     * problem downloading the data, etc..
981
     * @return array|string  On success : (array)  The headers.
982
     *                       On failure : (object) PEAR_Error.
983
     *                       On decompress failure: (string) error message
984
     */
985
    protected function &_getXFeatureTextResponse()
986
    {
987
        $possibleTerm = false;
988
        $data = null;
989
990
        while (! feof($this->_socket)) {
991
992
            // Did we find a possible ending ? (.\r\n)
993
            if ($possibleTerm) {
994
995
                // Loop, sleeping shortly, to allow the server time to upload data, if it has any.
996
                for ($i = 0; $i < 3; $i++) {
997
                    // If the socket is really empty, fGets will get stuck here, so set the socket to non blocking in case.
998
                    stream_set_blocking($this->_socket, 0);
999
1000
                    // Now try to download from the socket.
1001
                    $buffer = fgets($this->_socket);
1002
1003
                    // And set back the socket to blocking.
1004
                    stream_set_blocking($this->_socket, 1);
1005
1006
                    // Don't sleep on last iteration.
1007
                    if (! empty($buffer)) {
1008
                        break;
1009
                    }
1010
                    if ($i < 2) {
1011
                        usleep(10000);
1012
                    }
1013
                }
1014
1015
                // If the buffer was really empty, then we know $possibleTerm was the real ending.
1016
                if (empty($buffer)) {
1017
                    // Remove .\r\n from end, decompress data.
1018
                    $deComp = @gzuncompress(mb_substr($data, 0, -3, '8bit'));
1019
1020
                    if (! empty($deComp)) {
1021
                        $bytesReceived = \strlen($data);
1022
                        if ($this->_echo && $bytesReceived > 10240) {
1023
                            $this->colorCli->primaryOver(
1024
                                    'Received '.round($bytesReceived / 1024).
1025
                                    'KB from group ('.$this->group().').'
1026
                                );
1027
                        }
1028
1029
                        // Split the string of headers into an array of individual headers, then return it.
1030
                        $deComp = explode("\r\n", trim($deComp));
1031
1032
                        return $deComp;
1033
                    }
1034
                    $message = 'Decompression of OVER headers failed.';
1035
1036
                    $message = $this->throwError($this->colorCli->error($message), 1000);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1037
1038
                    return $message;
1039
                }
1040
                // The buffer was not empty, so we know this was not the real ending, so reset $possibleTerm.
1041
                $possibleTerm = false;
1042
            } else {
1043
                // Get data from the stream.
1044
                $buffer = fgets($this->_socket);
1045
            }
1046
1047
            // If we got no data at all try one more time to pull data.
1048
            if (empty($buffer)) {
1049
                usleep(10000);
1050
                $buffer = fgets($this->_socket);
1051
1052
                // If wet got nothing again, return error.
1053
                if (empty($buffer)) {
1054
                    $message = 'Error fetching data from usenet server while downloading OVER headers.';
1055
1056
                    $message = $this->throwError($this->colorCli->error($message), 1000);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1057
1058
                    return $message;
1059
                }
1060
            }
1061
1062
            // Append current buffer to rest of buffer.
1063
            $data .= $buffer;
1064
1065
            // Check if we have the ending (.\r\n)
1066
            if (substr($buffer, -3) === ".\r\n") {
1067
                // We have a possible ending, next loop check if it is.
1068
                $possibleTerm = true;
1069
            }
1070
        }
1071
1072
        $message = 'Unspecified error while downloading OVER headers.';
1073
        $message = $this->throwError($this->colorCli->error($message), 1000);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->error($message) targeting Blacklight\ColorCLI::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1074
1075
        return $message;
1076
    }
1077
1078
    /**
1079
     * Check if the Message-ID has the required opening and closing brackets.
1080
     *
1081
     * @param  string $messageID The Message-ID with or without brackets.
1082
     *
1083
     * @return string            Message-ID with brackets.
1084
     */
1085
    protected function _formatMessageID($messageID): string
1086
    {
1087
        $messageID = (string) $messageID;
1088
        if ($messageID === '') {
1089
            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...
1090
        }
1091
1092
        // Check if the first char is <, if not add it.
1093
        if ($messageID[0] !== '<') {
1094
            $messageID = ('<'.$messageID);
1095
        }
1096
1097
        // Check if the last char is >, if not add it.
1098
        if (substr($messageID, -1) !== '>') {
1099
            $messageID .= '>';
1100
        }
1101
1102
        return $messageID;
1103
    }
1104
1105
    /**
1106
     * Download an article body (an article without the header).
1107
     *
1108
     * @param string $groupName  The name of the group the article is in.
1109
     * @param mixed  $identifier (string) The message-ID of the article to download.
1110
     *                           (int)    The article number.
1111
     *
1112
     * @return string On success : (string) The article's body.
1113
     * @throws \Exception
1114
     *               On failure : (object) PEAR_Error.
1115
     */
1116
    protected function _getMessage($groupName, $identifier): ?string
1117
    {
1118
        // Make sure the requested group is already selected, if not select it.
1119
        if ($this->group() !== $groupName) {
1120
            // Select the group.
1121
            $summary = $this->selectGroup($groupName);
1122
            // If there was an error selecting the group, return PEAR error object.
1123
            if (self::isError($summary)) {
1124
                return $summary;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $summary could return the type array|array<string,string> which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
1125
            }
1126
        }
1127
1128
        // Check if this is an article number or message-id.
1129
        if (! is_numeric($identifier)) {
1130
            // It's a message-id so check if it has the triangular brackets.
1131
            $identifier = $this->_formatMessageID($identifier);
1132
        }
1133
1134
        // Tell the news server we want the body of an article.
1135
        $response = $this->_sendCommand('BODY '.$identifier);
1136
        if (self::isError($response)) {
1137
            return $response;
1138
        }
1139
1140
        $body = '';
1141
        if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) {
1142
1143
            // Continue until connection is lost
1144
            while (! feof($this->_socket)) {
1145
1146
                // Retrieve and append up to 1024 characters from the server.
1147
                $line = fgets($this->_socket, 1024);
1148
1149
                // If the socket is empty/ an error occurs, false is returned.
1150
                // Since the socket is blocking, the socket should not be empty, so it's definitely an error.
1151
                if ($line === false) {
1152
                    return $this->throwError('Failed to read line from socket.', null);
1153
                }
1154
1155
                // Check if the line terminates the text response.
1156
                if ($line === ".\r\n") {
1157
1158
                    // Attempt to yEnc decode and return the body.
1159
                    return PhpYenc::decodeIgnore($body);
1160
                }
1161
1162
                // Check for line that starts with double period, remove one.
1163
                if (strpos($line, '.') === 0 && $line[1] === '.') {
1164
                    $line = substr($line, 1);
1165
                }
1166
1167
                // Add the line to the rest of the lines.
1168
                $body .= $line;
1169
            }
1170
1171
            return $this->throwError('End of stream! Connection lost?', null);
1172
        }
1173
1174
        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...
1175
    }
1176
1177
    /**
1178
     * Check if we are still connected. Reconnect if not.
1179
     *
1180
     * @param  bool $reSelectGroup Select back the group after connecting?
1181
     *
1182
     * @return mixed On success: (bool)   True;
1183
     * @throws \Exception
1184
     *               On failure: (object) PEAR_Error
1185
     */
1186
    protected function _checkConnection($reSelectGroup = true)
1187
    {
1188
        $currentGroup = $this->_currentGroup;
1189
        // Check if we are connected.
1190
        if (parent::_isConnected()) {
1191
            $retVal = true;
1192
        } else {
1193
            switch ($this->_currentServer) {
1194
                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...
1195
                    if (\is_resource($this->_socket)) {
1196
                        $this->doQuit(true);
1197
                    }
1198
                    $retVal = $this->doConnect();
1199
                    break;
1200
                case env('NNTP_SERVER_A'):
1201
                    if (\is_resource($this->_socket)) {
1202
                        $this->doQuit(true);
1203
                    }
1204
                    $retVal = $this->doConnect(true, true);
1205
                    break;
1206
                default:
1207
                    $retVal = $this->throwError('Wrong server constant used in NNTP checkConnection()!');
1208
            }
1209
1210
            if ($retVal === true && $reSelectGroup) {
1211
                $group = $this->selectGroup($currentGroup);
1212
                if (self::isError($group)) {
1213
                    $retVal = $group;
1214
                }
1215
            }
1216
        }
1217
1218
        return $retVal;
1219
    }
1220
1221
    /**
1222
     * Verify NNTP error code and return PEAR error.
1223
     *
1224
     * @param int $response NET_NNTP Response code
1225
     *
1226
     * @return object PEAR error
1227
     */
1228
    protected function _handleErrorResponse($response)
1229
    {
1230
        switch ($response) {
1231
            // 381, RFC2980: 'More authentication information required'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1232
            case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_CONTINUE:
1233
                return $this->throwError('More authentication information required', $response, $this->_currentStatusResponse());
1234
            // 400, RFC977: 'Service discontinued'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1235
            case NET_NNTP_PROTOCOL_RESPONSECODE_DISCONNECTING_FORCED:
1236
                return $this->throwError('Server refused connection', $response, $this->_currentStatusResponse());
1237
            // 411, RFC977: 'no such news group'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1238
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_GROUP:
1239
                return $this->throwError('No such news group on server', $response, $this->_currentStatusResponse());
1240
            // 412, RFC2980: 'No news group current selected'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1241
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED:
1242
                return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse());
1243
            // 420, RFC2980: 'Current article number is invalid'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1244
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED:
1245
                return $this->throwError('Current article number is invalid', $response, $this->_currentStatusResponse());
1246
            // 421, RFC977: 'no next article in this group'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1247
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_NEXT_ARTICLE:
1248
                return $this->throwError('No next article in this group', $response, $this->_currentStatusResponse());
1249
            // 422, RFC977: 'no previous article in this group'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1250
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_PREVIOUS_ARTICLE:
1251
                return $this->throwError('No previous article in this group', $response, $this->_currentStatusResponse());
1252
            // 423, RFC977: 'No such article number in this group'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1253
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER:
1254
                return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse());
1255
            // 430, RFC977: 'No such article found'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1256
            case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID:
1257
                return $this->throwError('No such article found', $response, $this->_currentStatusResponse());
1258
            // 435, RFC977: 'Article not wanted'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1259
            case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_UNWANTED:
1260
                return $this->throwError('Article not wanted', $response, $this->_currentStatusResponse());
1261
            // 436, RFC977: 'Transfer failed - try again later'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1262
            case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_FAILURE:
1263
                return $this->throwError('Transfer failed - try again later', $response, $this->_currentStatusResponse());
1264
            // 437, RFC977: 'Article rejected - do not try again'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1265
            case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_REJECTED:
1266
                return $this->throwError('Article rejected - do not try again', $response, $this->_currentStatusResponse());
1267
            // 440, RFC977: 'posting not allowed'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1268
            case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_PROHIBITED:
1269
                return $this->throwError('Posting not allowed', $response, $this->_currentStatusResponse());
1270
            // 441, RFC977: 'posting failed'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1271
            case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_FAILURE:
1272
                return $this->throwError('Posting failed', $response, $this->_currentStatusResponse());
1273
            // 481, RFC2980: 'Groups and descriptions unavailable'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1274
            case NET_NNTP_PROTOCOL_RESPONSECODE_XGTITLE_GROUPS_UNAVAILABLE:
1275
                return $this->throwError('Groups and descriptions unavailable', $response, $this->_currentStatusResponse());
1276
            // 482, RFC2980: 'Authentication rejected'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1277
            case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_REJECTED:
1278
                return $this->throwError('Authentication rejected', $response, $this->_currentStatusResponse());
1279
            // 500, RFC977: 'Command not recognized'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1280
            case NET_NNTP_PROTOCOL_RESPONSECODE_UNKNOWN_COMMAND:
1281
                return $this->throwError('Command not recognized', $response, $this->_currentStatusResponse());
1282
            // 501, RFC977: 'Command syntax error'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1283
            case NET_NNTP_PROTOCOL_RESPONSECODE_SYNTAX_ERROR:
1284
                return $this->throwError('Command syntax error', $response, $this->_currentStatusResponse());
1285
            // 502, RFC2980: 'No permission'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1286
            case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_PERMITTED:
1287
                return $this->throwError('No permission', $response, $this->_currentStatusResponse());
1288
            // 503, RFC2980: 'Program fault - command not performed'
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1289
            case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_SUPPORTED:
1290
                return $this->throwError('Internal server error, function not performed', $response, $this->_currentStatusResponse());
1291
            // RFC4642: 'Can not initiate TLS negotiation'
1292
            case NET_NNTP_PROTOCOL_RESPONSECODE_TLS_FAILED_NEGOTIATION:
1293
                return $this->throwError('Can not initiate TLS negotiation', $response, $this->_currentStatusResponse());
1294
            default:
1295
                $text = $this->_currentStatusResponse();
1296
1297
                return $this->throwError("Unexpected response: '$text'", $response, $text);
1298
        }
1299
    }
1300
1301
    /**
1302
     * Connect to a NNTP server
1303
     *
1304
     * @param string $host                   (optional) The address of the NNTP-server to connect to, defaults to 'localhost'.
1305
     * @param mixed  $encryption             (optional) Use TLS/SSL on the connection?
1306
     *                                       (string) 'tcp'                 => Use no encryption.
1307
     *                                       'ssl', 'sslv3', 'tls' => Use encryption.
1308
     *                                       (null)|(false) Use no encryption.
1309
     * @param int    $port                   (optional) The port number to connect to, defaults to 119.
1310
     * @param int    $timeout                (optional) How many seconds to wait before giving up when connecting.
1311
     * @param int    $socketTimeout          (optional) How many seconds to wait before timing out the (blocked) socket.
1312
     *
1313
     * @return mixed (bool)   On success: True when posting allowed, otherwise false.
1314
     *               (object) On failure: pear_error
1315
     */
1316
    public function connect($host = null, $encryption = null, $port = null, $timeout = 15, $socketTimeout = 120)
1317
    {
1318
        if ($this->_isConnected()) {
1319
            return $this->throwError('Already connected, disconnect first!', null);
1320
        }
1321
        // v1.0.x API
1322
        if (is_int($encryption)) {
1323
            trigger_error('You are using deprecated API v1.0 in Net_NNTP_Protocol_Client: connect() !', E_USER_NOTICE);
1324
            $port = $encryption;
1325
            $encryption = false;
1326
        }
1327
        if ($host === null) {
1328
            $host = 'localhost';
1329
        }
1330
        // Choose transport based on encryption, and if no port is given, use default for that encryption.
1331
        switch ($encryption) {
1332
            case null:
1333
            case 'tcp':
1334
            case false:
1335
                $transport = 'tcp';
1336
                $port = $port ?? 119;
1337
                break;
1338
            case 'ssl':
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...
1339
            case 'tls':
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...
1340
                $transport = $encryption;
1341
                $port = $port ?? 563;
1342
                break;
1343
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
1344
                $message = '$encryption parameter must be either tcp, tls, ssl.';
1345
                trigger_error($message, E_USER_ERROR);
1346
                return $this->throwError($message);
1347
        }
1348
        // Attempt to connect to usenet.
1349
        $socket = stream_socket_client(
1350
            $transport . '://' . $host . ':' . $port,
1351
            $errorNumber,
1352
            $errorString,
1353
            $timeout,
1354
            STREAM_CLIENT_CONNECT,
1355
            stream_context_create(Utility::streamSslContextOptions())
1356
        );
1357
        if ($socket === false) {
1358
            $message = "Connection to $transport://$host:$port failed.";
1359
            if (preg_match('/tls|ssl/', $transport)) {
1360
                $message .= ' Try disabling SSL/TLS, and/or try a different port.';
1361
            }
1362
            $message .= ' [ERROR ' . $errorNumber . ': ' . $errorString . ']';
1363
            if ($this->_logger) {
1364
                $this->_logger->notice($message);
1365
            }
1366
            return $this->throwError($message);
1367
        }
1368
        // Store the socket resource as property.
1369
        $this->_socket = $socket;
1370
        $this->_socketTimeout = (is_numeric($socketTimeout) ? $socketTimeout : $this->_socketTimeout);
0 ignored issues
show
introduced by
The condition is_numeric($socketTimeout) is always true.
Loading history...
1371
        // Set the socket timeout.
1372
        stream_set_timeout($this->_socket, $this->_socketTimeout);
1373
        if ($this->_logger) {
1374
            $this->_logger->info("Connection to $transport://$host:$port has been established.");
1375
        }
1376
        // Retrieve the server's initial response.
1377
        $response = $this->_getStatusResponse();
1378
        if (self::isError($response)) {
1379
            return $response;
1380
        }
1381
        switch ($response) {
1382
            // 200, Posting allowed
1383
            case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_ALLOWED:
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...
1384
                // TODO: Set some variable before return
1385
                return true;
1386
            // 201, Posting NOT allowed
1387
            case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_PROHIBITED:
1388
                if ($this->_logger) {
1389
                    $this->_logger->info('Posting not allowed!');
1390
                }
1391
                // TODO: Set some variable before return
1392
                return false;
1393
            default:
1394
                return $this->_handleErrorResponse($response);
1395
        }
1396
    }
1397
1398
    /**
1399
     * Test whether we are connected or not.
1400
     *
1401
     * @param bool $feof Check for the end of file pointer.
1402
     *
1403
     * @return bool true or false
1404
     */
1405
    public function _isConnected($feof = true): bool
1406
    {
1407
        return (is_resource($this->_socket) && ($feof ? ! feof($this->_socket) : true));
1408
    }
1409
}
1410