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'); |
|
|
|
|
161
|
|
|
$this->_currentPort = env('NNTP_PORT'); |
|
|
|
|
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); |
|
|
|
|
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)); |
|
|
|
|
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)); |
|
|
|
|
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)); |
|
|
|
|
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(); |
|
|
|
|
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)); |
|
|
|
|
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)); |
|
|
|
|
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)); |
|
|
|
|
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)); |
|
|
|
|
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()."'"; |
|
|
|
|
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(); |
|
|
|
|
967
|
|
|
} |
968
|
|
|
|
969
|
|
|
return parent::_getTextResponse(); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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; |
|
|
|
|
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; |
|
|
|
|
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); |
|
|
|
|
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'): |
|
|
|
|
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' |
|
|
|
|
1232
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_CONTINUE: |
1233
|
|
|
return $this->throwError('More authentication information required', $response, $this->_currentStatusResponse()); |
1234
|
|
|
// 400, RFC977: 'Service discontinued' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
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' |
|
|
|
|
1268
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_PROHIBITED: |
1269
|
|
|
return $this->throwError('Posting not allowed', $response, $this->_currentStatusResponse()); |
1270
|
|
|
// 441, RFC977: 'posting failed' |
|
|
|
|
1271
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_FAILURE: |
1272
|
|
|
return $this->throwError('Posting failed', $response, $this->_currentStatusResponse()); |
1273
|
|
|
// 481, RFC2980: 'Groups and descriptions unavailable' |
|
|
|
|
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' |
|
|
|
|
1277
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_REJECTED: |
1278
|
|
|
return $this->throwError('Authentication rejected', $response, $this->_currentStatusResponse()); |
1279
|
|
|
// 500, RFC977: 'Command not recognized' |
|
|
|
|
1280
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_UNKNOWN_COMMAND: |
1281
|
|
|
return $this->throwError('Command not recognized', $response, $this->_currentStatusResponse()); |
1282
|
|
|
// 501, RFC977: 'Command syntax error' |
|
|
|
|
1283
|
|
|
case NET_NNTP_PROTOCOL_RESPONSECODE_SYNTAX_ERROR: |
1284
|
|
|
return $this->throwError('Command syntax error', $response, $this->_currentStatusResponse()); |
1285
|
|
|
// 502, RFC2980: 'No permission' |
|
|
|
|
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' |
|
|
|
|
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': |
|
|
|
|
1339
|
|
|
case 'tls': |
|
|
|
|
1340
|
|
|
$transport = $encryption; |
1341
|
|
|
$port = $port ?? 563; |
1342
|
|
|
break; |
1343
|
|
|
default: |
|
|
|
|
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); |
|
|
|
|
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: |
|
|
|
|
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
|
|
|
|
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 theid
property of an instance of theAccount
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.