| Total Complexity | 221 |
| Total Lines | 1368 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like NNTPService 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 NNTPService, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 23 | class NNTPService extends \Net_NNTP_Client |
||
| 24 | { |
||
| 25 | protected bool $_debugBool; |
||
| 26 | |||
| 27 | protected bool $_echo; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * Does the server support XFeature GZip header compression? |
||
| 31 | */ |
||
| 32 | protected bool $_compressionSupported = true; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * Is header compression enabled for the session? |
||
| 36 | */ |
||
| 37 | protected bool $_compressionEnabled = false; |
||
| 38 | |||
| 39 | /** |
||
| 40 | * Currently selected group. |
||
| 41 | */ |
||
| 42 | protected string $_currentGroup = ''; |
||
| 43 | |||
| 44 | protected string $_currentPort = 'NNTP_PORT'; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * Address of the current NNTP server. |
||
| 48 | */ |
||
| 49 | protected string $_currentServer = 'NNTP_SERVER'; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * Are we allowed to post to usenet? |
||
| 53 | */ |
||
| 54 | protected bool $_postingAllowed = false; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * How many times should we try to reconnect to the NNTP server? |
||
| 58 | */ |
||
| 59 | protected int $_nntpRetries; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * How many connections should we use on primary NNTP server. |
||
| 63 | */ |
||
| 64 | protected string $_primaryNntpConnections; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * How many connections should we use on alternate NNTP server. |
||
| 68 | */ |
||
| 69 | protected string $_alternateNntpConnections; |
||
| 70 | |||
| 71 | /** |
||
| 72 | * How many connections do we use on primary NNTP server. |
||
| 73 | */ |
||
| 74 | protected int $_primaryCurrentNntpConnections; |
||
| 75 | |||
| 76 | /** |
||
| 77 | * How many connections do we use on alternate NNTP server. |
||
| 78 | */ |
||
| 79 | protected int $_alternateCurrentNntpConnections; |
||
| 80 | |||
| 81 | /** |
||
| 82 | * Seconds to wait for the blocking socket to timeout. |
||
| 83 | */ |
||
| 84 | protected int $_socketTimeout = 120; |
||
| 85 | |||
| 86 | protected Tmux $_tmux; |
||
| 87 | |||
| 88 | /** |
||
| 89 | * @var resource|null |
||
| 90 | */ |
||
| 91 | protected $_socket = null; |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Cached config values for performance. |
||
| 95 | */ |
||
| 96 | protected string $_configServer; |
||
| 97 | |||
| 98 | protected string $_configAlternateServer; |
||
| 99 | |||
| 100 | protected int $_configPort; |
||
| 101 | |||
| 102 | protected int $_configAlternatePort; |
||
| 103 | |||
| 104 | protected bool $_configSsl; |
||
| 105 | |||
| 106 | protected bool $_configAlternateSsl; |
||
| 107 | |||
| 108 | protected string $_configUsername; |
||
| 109 | |||
| 110 | protected string $_configPassword; |
||
| 111 | |||
| 112 | protected string $_configAlternateUsername; |
||
| 113 | |||
| 114 | protected string $_configAlternatePassword; |
||
| 115 | |||
| 116 | protected int $_configSocketTimeout; |
||
| 117 | |||
| 118 | protected int $_configAlternateSocketTimeout; |
||
| 119 | |||
| 120 | protected bool $_configCompressedHeaders; |
||
| 121 | |||
| 122 | /** |
||
| 123 | * If on unix, hide yydecode CLI output. |
||
| 124 | */ |
||
| 125 | protected string $_yEncSilence; |
||
| 126 | |||
| 127 | /** |
||
| 128 | * Path to temp yEnc input storage file. |
||
| 129 | */ |
||
| 130 | protected string $_yEncTempInput; |
||
| 131 | |||
| 132 | /** |
||
| 133 | * Path to temp yEnc output storage file. |
||
| 134 | */ |
||
| 135 | protected string $_yEncTempOutput; |
||
| 136 | |||
| 137 | /** |
||
| 138 | * YEnc encoding/decoding service. |
||
| 139 | */ |
||
| 140 | protected YencService $_yencService; |
||
| 141 | |||
| 142 | /** |
||
| 143 | * Create a new NNTP service instance. |
||
| 144 | */ |
||
| 145 | public function __construct(?Tmux $tmux = null, ?YencService $yencService = null) |
||
| 146 | { |
||
| 147 | parent::__construct(); |
||
| 148 | |||
| 149 | $this->_echo = config('nntmux.echocli'); |
||
| 150 | $this->_tmux = $tmux ?? new Tmux; |
||
| 151 | $this->_yencService = $yencService ?? app(YencService::class); |
||
| 152 | $this->_nntpRetries = Settings::settingValue('nntpretries') !== '' ? (int) Settings::settingValue('nntpretries') : 0 + 1; |
||
| 153 | |||
| 154 | $this->initializeConfig(); |
||
| 155 | } |
||
| 156 | |||
| 157 | /** |
||
| 158 | * Initialize configuration values from Laravel config. |
||
| 159 | */ |
||
| 160 | protected function initializeConfig(): void |
||
| 161 | { |
||
| 162 | $this->_configServer = config('nntmux_nntp.server'); |
||
| 163 | $this->_configAlternateServer = config('nntmux_nntp.alternate_server'); |
||
| 164 | $this->_configPort = (int) config('nntmux_nntp.port'); |
||
| 165 | $this->_configAlternatePort = (int) config('nntmux_nntp.alternate_server_port'); |
||
| 166 | $this->_configSsl = (bool) config('nntmux_nntp.ssl'); |
||
| 167 | $this->_configAlternateSsl = (bool) config('nntmux_nntp.alternate_server_ssl'); |
||
| 168 | $this->_configUsername = config('nntmux_nntp.username') ?? ''; |
||
| 169 | $this->_configPassword = config('nntmux_nntp.password') ?? ''; |
||
| 170 | $this->_configAlternateUsername = config('nntmux_nntp.alternate_server_username') ?? ''; |
||
| 171 | $this->_configAlternatePassword = config('nntmux_nntp.alternate_server_password') ?? ''; |
||
| 172 | $this->_configSocketTimeout = (int) (config('nntmux_nntp.socket_timeout') ?: $this->_socketTimeout); |
||
| 173 | $this->_configAlternateSocketTimeout = (int) (config('nntmux_nntp.alternate_server_socket_timeout') ?: $this->_socketTimeout); |
||
| 174 | $this->_configCompressedHeaders = (bool) config('nntmux_nntp.compressed_headers'); |
||
| 175 | |||
| 176 | $this->_currentPort = $this->_configPort; |
||
| 177 | $this->_currentServer = $this->_configServer; |
||
| 178 | $this->_primaryNntpConnections = config('nntmux_nntp.main_nntp_connections'); |
||
| 179 | $this->_alternateNntpConnections = config('nntmux_nntp.alternate_nntp_connections'); |
||
| 180 | $this->_selectedGroupSummary = null; |
||
| 181 | $this->_overviewFormatCache = null; |
||
| 182 | } |
||
| 183 | |||
| 184 | /** |
||
| 185 | * Destruct. |
||
| 186 | * Close the NNTP connection if still connected. |
||
| 187 | */ |
||
| 188 | public function __destruct() |
||
| 189 | { |
||
| 190 | $this->doQuit(); |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * Connect to a usenet server. |
||
| 195 | * |
||
| 196 | * @param bool $compression Should we attempt to enable XFeature Gzip compression on this connection? |
||
| 197 | * @param bool $alternate Use the alternate NNTP connection. |
||
| 198 | * @return mixed On success = (bool) Did we successfully connect to the usenet? |
||
| 199 | * |
||
| 200 | * @throws \Exception |
||
| 201 | * On failure = (object) PEAR_Error. |
||
| 202 | */ |
||
| 203 | public function doConnect(bool $compression = true, bool $alternate = false): mixed |
||
| 204 | { |
||
| 205 | $primaryUSP = [ |
||
| 206 | 'ip' => gethostbyname($this->_configServer), |
||
| 207 | 'port' => $this->_configPort, |
||
| 208 | ]; |
||
| 209 | $alternateUSP = [ |
||
| 210 | 'ip_a' => gethostbyname($this->_configAlternateServer), |
||
| 211 | 'port_a' => $this->_configAlternatePort, |
||
| 212 | ]; |
||
| 213 | $primaryConnections = $this->_tmux->getUSPConnections('primary', $primaryUSP); |
||
| 214 | $alternateConnections = $this->_tmux->getUSPConnections('alternate', $alternateUSP); |
||
| 215 | if ($this->_isConnected() && (($alternate && $this->_currentServer === $this->_configAlternateServer && ($this->_primaryNntpConnections < $alternateConnections['alternate']['active'])) || (! $alternate && $this->_currentServer === $this->_configServer && ($this->_primaryNntpConnections < $primaryConnections['primary']['active'])))) { |
||
| 216 | return true; |
||
| 217 | } |
||
| 218 | |||
| 219 | $this->doQuit(); |
||
| 220 | |||
| 221 | $ret = $connected = $cError = $aError = false; |
||
| 222 | |||
| 223 | // Set variables to connect based on if we are using the alternate provider or not. |
||
| 224 | if (! $alternate) { |
||
| 225 | $sslEnabled = $this->_configSsl; |
||
| 226 | $this->_currentServer = $this->_configServer; |
||
| 227 | $this->_currentPort = $this->_configPort; |
||
| 228 | $userName = $this->_configUsername; |
||
| 229 | $password = $this->_configPassword; |
||
| 230 | $socketTimeout = $this->_configSocketTimeout; |
||
| 231 | } else { |
||
| 232 | $sslEnabled = $this->_configAlternateSsl; |
||
| 233 | $this->_currentServer = $this->_configAlternateServer; |
||
| 234 | $this->_currentPort = $this->_configAlternatePort; |
||
| 235 | $userName = $this->_configAlternateUsername; |
||
| 236 | $password = $this->_configAlternatePassword; |
||
| 237 | $socketTimeout = $this->_configAlternateSocketTimeout; |
||
| 238 | } |
||
| 239 | |||
| 240 | $enc = ($sslEnabled ? ' (ssl)' : ' (non-ssl)'); |
||
| 241 | $sslEnabled = ($sslEnabled ? 'tls' : false); |
||
| 242 | |||
| 243 | // Try to connect until we run of out tries. |
||
| 244 | $retries = $this->_nntpRetries; |
||
| 245 | while (true) { |
||
| 246 | $retries--; |
||
| 247 | $authenticated = false; |
||
| 248 | |||
| 249 | // If we are not connected, try to connect. |
||
| 250 | if (! $connected) { |
||
| 251 | $ret = $this->connect($this->_currentServer, $sslEnabled, $this->_currentPort, 5, $socketTimeout); |
||
|
|
|||
| 252 | } |
||
| 253 | // Check if we got an error while connecting. |
||
| 254 | $cErr = self::isError($ret); |
||
| 255 | |||
| 256 | // If no error, we are connected. |
||
| 257 | if (! $cErr) { |
||
| 258 | // Say that we are connected so we don't retry. |
||
| 259 | $connected = true; |
||
| 260 | // When there is no error it returns bool if we are allowed to post or not. |
||
| 261 | $this->_postingAllowed = $ret; |
||
| 262 | } elseif (! $cError) { |
||
| 263 | $cError = $ret->getMessage(); |
||
| 264 | } |
||
| 265 | |||
| 266 | // If error, try to connect again. |
||
| 267 | if ($cErr && $retries > 0) { |
||
| 268 | continue; |
||
| 269 | } |
||
| 270 | |||
| 271 | // If we have no more retries and could not connect, return an error. |
||
| 272 | if ($retries === 0 && ! $connected) { |
||
| 273 | $message = |
||
| 274 | 'Cannot connect to server '. |
||
| 275 | $this->_currentServer. |
||
| 276 | $enc. |
||
| 277 | ': '. |
||
| 278 | $cError; |
||
| 279 | |||
| 280 | return $this->throwError(cli()->error($message)); |
||
| 281 | } |
||
| 282 | |||
| 283 | // If we are connected, try to authenticate. |
||
| 284 | if ($connected) { |
||
| 285 | // If the username is empty it probably means the server does not require a username. |
||
| 286 | if ($userName === '') { |
||
| 287 | $authenticated = true; |
||
| 288 | |||
| 289 | // Try to authenticate to usenet. |
||
| 290 | } else { |
||
| 291 | $ret2 = $this->authenticate($userName, $password); |
||
| 292 | |||
| 293 | // Check if there was an error authenticating. |
||
| 294 | $aErr = self::isError($ret2); |
||
| 295 | |||
| 296 | // If there was no error, then we are authenticated. |
||
| 297 | if (! $aErr) { |
||
| 298 | $authenticated = true; |
||
| 299 | } elseif (! $aError) { |
||
| 300 | $aError = $ret2->getMessage(); |
||
| 301 | } |
||
| 302 | |||
| 303 | // If error, try to authenticate again. |
||
| 304 | if ($aErr && $retries > 0) { |
||
| 305 | continue; |
||
| 306 | } |
||
| 307 | |||
| 308 | // If we ran out of retries, return an error. |
||
| 309 | if ($retries === 0 && ! $authenticated) { |
||
| 310 | $message = |
||
| 311 | 'Cannot authenticate to server '. |
||
| 312 | $this->_currentServer. |
||
| 313 | $enc. |
||
| 314 | ' - '. |
||
| 315 | $userName. |
||
| 316 | ' ('.$aError.')'; |
||
| 317 | |||
| 318 | return $this->throwError(cli()->error($message)); |
||
| 319 | } |
||
| 320 | } |
||
| 321 | } |
||
| 322 | // If we are connected and authenticated, try enabling compression if we have it enabled. |
||
| 323 | if ($connected && $authenticated) { |
||
| 324 | // Check if we should use compression on the connection. |
||
| 325 | if (! $compression || ! $this->_configCompressedHeaders) { |
||
| 326 | $this->_compressionSupported = false; |
||
| 327 | } |
||
| 328 | |||
| 329 | return true; |
||
| 330 | } |
||
| 331 | // If we reached this point and have not connected after all retries, break out of the loop. |
||
| 332 | if ($retries === 0) { |
||
| 333 | break; |
||
| 334 | } |
||
| 335 | |||
| 336 | // Sleep .4 seconds between retries. |
||
| 337 | usleep(400000); |
||
| 338 | } |
||
| 339 | // If we somehow got out of the loop, return an error. |
||
| 340 | $message = 'Unable to connect to '.$this->_currentServer.$enc; |
||
| 341 | |||
| 342 | return $this->throwError(cli()->error($message)); |
||
| 343 | } |
||
| 344 | |||
| 345 | /** |
||
| 346 | * Disconnect from the current NNTP server. |
||
| 347 | * |
||
| 348 | * @param bool $force Force quit even if not connected? |
||
| 349 | * @return mixed On success : (bool) Did we successfully disconnect from usenet? |
||
| 350 | * On Failure : (object) PEAR_Error. |
||
| 351 | */ |
||
| 352 | public function doQuit(bool $force = false): mixed |
||
| 353 | { |
||
| 354 | $this->_resetProperties(); |
||
| 355 | |||
| 356 | // Check if we are connected to usenet. |
||
| 357 | if ($force || $this->_isConnected(false)) { |
||
| 358 | // Disconnect from usenet. |
||
| 359 | return $this->disconnect(); |
||
| 360 | } |
||
| 361 | |||
| 362 | return true; |
||
| 363 | } |
||
| 364 | |||
| 365 | /** |
||
| 366 | * Reset some properties when disconnecting from usenet. |
||
| 367 | */ |
||
| 368 | protected function _resetProperties(): void |
||
| 369 | { |
||
| 370 | $this->_compressionEnabled = false; |
||
| 371 | $this->_compressionSupported = true; |
||
| 372 | $this->_currentGroup = ''; |
||
| 373 | $this->_postingAllowed = false; |
||
| 374 | $this->_selectedGroupSummary = null; |
||
| 375 | $this->_overviewFormatCache = null; |
||
| 376 | $this->_socket = null; |
||
| 377 | } |
||
| 378 | |||
| 379 | /** |
||
| 380 | * Attempt to enable compression if the admin enabled the site setting. |
||
| 381 | * |
||
| 382 | * @note This can be used to enable compression if the server was connected without compression. |
||
| 383 | * |
||
| 384 | * @throws \Exception |
||
| 385 | */ |
||
| 386 | public function enableCompression(): void |
||
| 387 | { |
||
| 388 | if (! $this->_configCompressedHeaders) { |
||
| 389 | return; |
||
| 390 | } |
||
| 391 | $this->_enableCompression(); |
||
| 392 | } |
||
| 393 | |||
| 394 | /** |
||
| 395 | * @param string $group Name of the group to select. |
||
| 396 | * @param mixed $articles (optional) experimental! When true the article numbers is returned in 'articles'. |
||
| 397 | * @param bool $force Force a refresh to get updated data from the usenet server. |
||
| 398 | * @return mixed On success : (array) Group information. |
||
| 399 | * |
||
| 400 | * @throws \Exception |
||
| 401 | * On failure : (object) PEAR_Error. |
||
| 402 | */ |
||
| 403 | public function selectGroup(string $group, mixed $articles = false, bool $force = false): mixed |
||
| 404 | { |
||
| 405 | $connected = $this->_checkConnection(false); |
||
| 406 | if ($connected !== true) { |
||
| 407 | return $connected; |
||
| 408 | } |
||
| 409 | |||
| 410 | // Check if the current selected group is the same, or if we have not selected a group or if a fresh summary is wanted. |
||
| 411 | if ($force || $this->_currentGroup !== $group || $this->_selectedGroupSummary === null) { |
||
| 412 | $this->_currentGroup = $group; |
||
| 413 | |||
| 414 | return parent::selectGroup($group, $articles); |
||
| 415 | } |
||
| 416 | |||
| 417 | return $this->_selectedGroupSummary; |
||
| 418 | } |
||
| 419 | |||
| 420 | /** |
||
| 421 | * Fetch an overview of article(s) in the currently selected group. |
||
| 422 | * |
||
| 423 | * @return mixed On success : (array) Multidimensional array with article headers. |
||
| 424 | * |
||
| 425 | * @throws \Exception |
||
| 426 | * On failure : (object) PEAR_Error. |
||
| 427 | */ |
||
| 428 | public function getOverview($range = null, $names = true, $forceNames = true): mixed |
||
| 429 | { |
||
| 430 | $connected = $this->_checkConnection(); |
||
| 431 | if ($connected !== true) { |
||
| 432 | return $connected; |
||
| 433 | } |
||
| 434 | |||
| 435 | // Enabled header compression if not enabled. |
||
| 436 | $this->_enableCompression(); |
||
| 437 | |||
| 438 | return parent::getOverview($range, $names, $forceNames); |
||
| 439 | } |
||
| 440 | |||
| 441 | /** |
||
| 442 | * Pass a XOVER command to the NNTP provider, return array of articles using the overview format as array keys. |
||
| 443 | * |
||
| 444 | * @note This is a faster implementation of getOverview. |
||
| 445 | * |
||
| 446 | * Example successful return: |
||
| 447 | * array(9) { |
||
| 448 | * 'Number' => string(9) "679871775" |
||
| 449 | * 'Subject' => string(18) "This is an example" |
||
| 450 | * 'From' => string(19) "[email protected]" |
||
| 451 | * 'Date' => string(24) "26 Jun 2014 13:08:22 GMT" |
||
| 452 | * 'Message-ID' => string(57) "<part1of1.uS*yYxQvtAYt$5t&wmE%[email protected]>" |
||
| 453 | * 'References' => string(0) "" |
||
| 454 | * 'Bytes' => string(3) "123" |
||
| 455 | * 'Lines' => string(1) "9" |
||
| 456 | * 'Xref' => string(66) "e alt.test:679871775" |
||
| 457 | * } |
||
| 458 | * |
||
| 459 | * @param string $range Range of articles to get the overview for. Examples follow: |
||
| 460 | * Single article number: "679871775" |
||
| 461 | * Range of article numbers: "679871775-679999999" |
||
| 462 | * All newer than article number: "679871775-" |
||
| 463 | * All older than article number: "-679871775" |
||
| 464 | * Message-ID: "<part1of1.uS*yYxQvtAYt$5t&wmE%[email protected]>" |
||
| 465 | * @return array|string|NNTPService Multi-dimensional Array of headers on success, PEAR object on failure. |
||
| 466 | * |
||
| 467 | * @throws \Exception |
||
| 468 | */ |
||
| 469 | public function getXOVER(string $range): array|string|NNTPService |
||
| 470 | { |
||
| 471 | // Check if we are still connected. |
||
| 472 | $connected = $this->_checkConnection(); |
||
| 473 | if ($connected !== true) { |
||
| 474 | return $connected; |
||
| 475 | } |
||
| 476 | |||
| 477 | // Enabled header compression if not enabled. |
||
| 478 | $this->_enableCompression(); |
||
| 479 | |||
| 480 | // Send XOVER command to NNTP with wanted articles. |
||
| 481 | $response = $this->_sendCommand('XOVER '.$range); |
||
| 482 | if (self::isError($response)) { |
||
| 483 | return $response; |
||
| 484 | } |
||
| 485 | |||
| 486 | // Verify the NNTP server got the right command, get the headers data. |
||
| 487 | if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_OVERVIEW_FOLLOWS) { |
||
| 488 | $data = $this->_getTextResponse(); |
||
| 489 | if (self::isError($data)) { |
||
| 490 | return $data; |
||
| 491 | } |
||
| 492 | } else { |
||
| 493 | return $this->_handleErrorResponse($response); |
||
| 494 | } |
||
| 495 | |||
| 496 | // Fetch the header overview format (for setting the array keys on the return array). |
||
| 497 | if ($this->_overviewFormatCache !== null && isset($this->_overviewFormatCache['Xref'])) { |
||
| 498 | $overview = $this->_overviewFormatCache; |
||
| 499 | } else { |
||
| 500 | $overview = $this->getOverviewFormat(false, true); |
||
| 501 | if (self::isError($overview)) { |
||
| 502 | return $overview; |
||
| 503 | } |
||
| 504 | $this->_overviewFormatCache = $overview; |
||
| 505 | } |
||
| 506 | |||
| 507 | // Pre-compute keys array and Xref position for faster processing |
||
| 508 | $keys = array_merge(['Number'], array_keys($overview)); |
||
| 509 | $keyCount = \count($keys); |
||
| 510 | $xrefIndex = array_search('Xref', $keys, true); |
||
| 511 | |||
| 512 | // Loop over strings of headers. |
||
| 513 | foreach ($data as $key => $header) { |
||
| 514 | // Split the individual headers by tab. |
||
| 515 | $parts = explode("\t", $header); |
||
| 516 | |||
| 517 | // Make sure it's not empty. |
||
| 518 | if ($parts === false || empty($parts)) { |
||
| 519 | continue; |
||
| 520 | } |
||
| 521 | |||
| 522 | // Build header array using pre-computed keys |
||
| 523 | $headerArray = []; |
||
| 524 | $partCount = \count($parts); |
||
| 525 | |||
| 526 | for ($i = 0; $i < $keyCount && $i < $partCount; $i++) { |
||
| 527 | $value = $parts[$i]; |
||
| 528 | // Strip "Xref: " prefix if this is the Xref field |
||
| 529 | if ($i === $xrefIndex && isset($value[5])) { |
||
| 530 | $value = substr($value, 6); |
||
| 531 | } |
||
| 532 | $headerArray[$keys[$i]] = $value; |
||
| 533 | } |
||
| 534 | |||
| 535 | // Add the individual header array back to the return array. |
||
| 536 | $data[$key] = $headerArray; |
||
| 537 | } |
||
| 538 | |||
| 539 | // Return the array of headers. |
||
| 540 | return $data; |
||
| 541 | } |
||
| 542 | |||
| 543 | /** |
||
| 544 | * Fetch valid groups. |
||
| 545 | * |
||
| 546 | * Returns a list of valid groups (that the client is permitted to select) and associated information. |
||
| 547 | * |
||
| 548 | * @param mixed $wildMat (optional) http://tools.ietf.org/html/rfc3977#section-4 |
||
| 549 | * @return array|string Pear error on failure, array with groups on success. |
||
| 550 | * |
||
| 551 | * @throws \Exception |
||
| 552 | */ |
||
| 553 | public function getGroups(mixed $wildMat = null): mixed |
||
| 554 | { |
||
| 555 | // Enabled header compression if not enabled. |
||
| 556 | $this->_enableCompression(); |
||
| 557 | |||
| 558 | return parent::getGroups($wildMat); |
||
| 559 | } |
||
| 560 | |||
| 561 | /** |
||
| 562 | * Download multiple article bodies and string them together. |
||
| 563 | * |
||
| 564 | * @param string $groupName The name of the group the articles are in. |
||
| 565 | * @param mixed $identifiers (string) Message-ID. |
||
| 566 | * (int) Article number. |
||
| 567 | * (array) Article numbers or Message-ID's (can contain both in the same array) |
||
| 568 | * @param bool $alternate Use the alternate NNTP provider? |
||
| 569 | * @return mixed On success : (string) The article bodies. |
||
| 570 | * |
||
| 571 | * @throws \Exception |
||
| 572 | * On failure : (object) PEAR_Error. |
||
| 573 | */ |
||
| 574 | public function getMessages(string $groupName, mixed $identifiers, bool $alternate = false): mixed |
||
| 575 | { |
||
| 576 | $connected = $this->_checkConnection(); |
||
| 577 | if ($connected !== true) { |
||
| 578 | return $connected; |
||
| 579 | } |
||
| 580 | |||
| 581 | // String to hold all the bodies. |
||
| 582 | $body = ''; |
||
| 583 | |||
| 584 | $aConnected = false; |
||
| 585 | $nntp = ($alternate ? new self : null); |
||
| 586 | |||
| 587 | // Check if the msgIds are in an array. |
||
| 588 | if (\is_array($identifiers)) { |
||
| 589 | $loops = $messageSize = 0; |
||
| 590 | |||
| 591 | // Loop over the message-ID's or article numbers. |
||
| 592 | foreach ($identifiers as $wanted) { |
||
| 593 | /* This is to attempt to prevent string size overflow. |
||
| 594 | * We get the size of 1 body in bytes, we increment the loop on every loop, |
||
| 595 | * then we multiply the # of loops by the first size we got and check if it |
||
| 596 | * exceeds 1.7 billion bytes (less than 2GB to give us headroom). |
||
| 597 | * If we exceed, return the data. |
||
| 598 | * If we don't do this, these errors are fatal. |
||
| 599 | */ |
||
| 600 | if ((++$loops * $messageSize) >= 1700000000) { |
||
| 601 | return $body; |
||
| 602 | } |
||
| 603 | |||
| 604 | // Download the body. |
||
| 605 | $message = $this->_getMessage($groupName, $wanted); |
||
| 606 | |||
| 607 | // Append the body to $body. |
||
| 608 | if (! self::isError($message)) { |
||
| 609 | $body .= $message; |
||
| 610 | |||
| 611 | if ($messageSize === 0) { |
||
| 612 | $messageSize = \strlen($message); |
||
| 613 | } |
||
| 614 | |||
| 615 | // If there is an error try the alternate provider or return the PEAR error. |
||
| 616 | } elseif ($alternate) { |
||
| 617 | if (! $aConnected) { |
||
| 618 | // Check if the current connected server is the alternate or not. |
||
| 619 | $aConnected = $this->_currentServer === $this->_configServer |
||
| 620 | ? $nntp->doConnect($this->_configCompressedHeaders, true) |
||
| 621 | : $nntp->doConnect(); |
||
| 622 | } |
||
| 623 | // If we connected successfully to usenet try to download the article body. |
||
| 624 | if ($aConnected === true) { |
||
| 625 | $newBody = $nntp->_getMessage($groupName, $wanted); |
||
| 626 | // Check if we got an error. |
||
| 627 | if ($nntp->isError($newBody)) { |
||
| 628 | if ($aConnected) { |
||
| 629 | $nntp->doQuit(); |
||
| 630 | } |
||
| 631 | // If we got some data, return it. |
||
| 632 | if ($body !== '') { |
||
| 633 | return $body; |
||
| 634 | } |
||
| 635 | |||
| 636 | // Return the error. |
||
| 637 | return $newBody; |
||
| 638 | } |
||
| 639 | // Append the alternate body to the main body. |
||
| 640 | $body .= $newBody; |
||
| 641 | } |
||
| 642 | } else { |
||
| 643 | // If we got some data, return it. |
||
| 644 | if ($body !== '') { |
||
| 645 | return $body; |
||
| 646 | } |
||
| 647 | |||
| 648 | return $message; |
||
| 649 | } |
||
| 650 | } |
||
| 651 | |||
| 652 | // If it's a string check if it's a valid message-ID. |
||
| 653 | } elseif (\is_string($identifiers) || is_numeric($identifiers)) { |
||
| 654 | $body = $this->_getMessage($groupName, $identifiers); |
||
| 655 | if ($alternate && self::isError($body)) { |
||
| 656 | $nntp->doConnect($this->_configCompressedHeaders, true); |
||
| 657 | $body = $nntp->_getMessage($groupName, $identifiers); |
||
| 658 | $aConnected = true; |
||
| 659 | } |
||
| 660 | |||
| 661 | // Else return an error. |
||
| 662 | } else { |
||
| 663 | $message = 'Wrong Identifier type, array, int or string accepted. This type of var was passed: '.gettype($identifiers); |
||
| 664 | |||
| 665 | return $this->throwError(cli()->error($message)); |
||
| 666 | } |
||
| 667 | |||
| 668 | if ($aConnected === true) { |
||
| 669 | $nntp->doQuit(); |
||
| 670 | } |
||
| 671 | |||
| 672 | return $body; |
||
| 673 | } |
||
| 674 | |||
| 675 | /** |
||
| 676 | * Download multiple article bodies by Message-ID only (no group selection), concatenating them. |
||
| 677 | * Falls back to alternate provider if enabled. Message-IDs are yEnc decoded. |
||
| 678 | * |
||
| 679 | * @param mixed $identifiers string|array Message-ID(s) (with or without < >) |
||
| 680 | * @param bool $alternate Use alternate NNTP server if primary fails for any ID. |
||
| 681 | * @return mixed string concatenated bodies on success, PEAR_Error object on total failure. |
||
| 682 | * |
||
| 683 | * @throws \Exception |
||
| 684 | */ |
||
| 685 | public function getMessagesByMessageID(mixed $identifiers, bool $alternate = false): mixed |
||
| 686 | { |
||
| 687 | $connected = $this->_checkConnection(false); // no need to reselect group |
||
| 688 | if ($connected !== true) { |
||
| 689 | return $connected; // PEAR error passthrough |
||
| 690 | } |
||
| 691 | |||
| 692 | $body = ''; |
||
| 693 | $aConnected = false; |
||
| 694 | $alt = ($alternate ? new self : null); |
||
| 695 | |||
| 696 | // Normalise to array for loop processing |
||
| 697 | $ids = is_array($identifiers) ? $identifiers : [$identifiers]; |
||
| 698 | |||
| 699 | $loops = 0; |
||
| 700 | $messageSize = 0; |
||
| 701 | foreach ($ids as $id) { |
||
| 702 | if ((++$loops * $messageSize) >= 1700000000) { // prevent huge string growth |
||
| 703 | return $body; |
||
| 704 | } |
||
| 705 | $msg = $this->_getMessageByMessageID($id); |
||
| 706 | if (! self::isError($msg)) { |
||
| 707 | $body .= $msg; |
||
| 708 | if ($messageSize === 0) { |
||
| 709 | $messageSize = strlen($msg); |
||
| 710 | } |
||
| 711 | |||
| 712 | continue; |
||
| 713 | } |
||
| 714 | // Primary failed, try alternate if requested |
||
| 715 | if ($alternate) { |
||
| 716 | if (! $aConnected) { |
||
| 717 | $aConnected = $this->_currentServer === $this->_configServer |
||
| 718 | ? $alt->doConnect($this->_configCompressedHeaders, true) |
||
| 719 | : $alt->doConnect(); |
||
| 720 | } |
||
| 721 | if ($aConnected === true) { |
||
| 722 | $altMsg = $alt->_getMessageByMessageID($id); |
||
| 723 | if ($alt->isError($altMsg)) { |
||
| 724 | if ($aConnected) { |
||
| 725 | $alt->doQuit(); |
||
| 726 | } |
||
| 727 | |||
| 728 | return $body !== '' ? $body : $altMsg; // return what we have or error |
||
| 729 | } |
||
| 730 | $body .= $altMsg; |
||
| 731 | } else { // alternate connect failed |
||
| 732 | return $body !== '' ? $body : $msg; // return collected or original error |
||
| 733 | } |
||
| 734 | } else { // no alternate |
||
| 735 | return $body !== '' ? $body : $msg; |
||
| 736 | } |
||
| 737 | } |
||
| 738 | |||
| 739 | if ($aConnected === true) { |
||
| 740 | $alt->doQuit(); |
||
| 741 | } |
||
| 742 | |||
| 743 | return $body; |
||
| 744 | } |
||
| 745 | |||
| 746 | /** |
||
| 747 | * Internal: fetch single article body by Message-ID (yEnc decoded) without selecting a group. |
||
| 748 | * Accepts article numbers but these require a group; will return error if numeric passed. |
||
| 749 | * |
||
| 750 | * @param mixed $identifier Message-ID or article number. |
||
| 751 | * @return mixed string body on success, PEAR_Error on failure. |
||
| 752 | * |
||
| 753 | * @throws \Exception |
||
| 754 | */ |
||
| 755 | protected function _getMessageByMessageID(mixed $identifier): mixed |
||
| 756 | { |
||
| 757 | // If numeric we cannot safely fetch without group context – delegate to existing path via error. |
||
| 758 | if (is_numeric($identifier)) { |
||
| 759 | return $this->throwError('Numeric article number requires group selection'); |
||
| 760 | } |
||
| 761 | $id = $this->_formatMessageID($identifier); |
||
| 762 | $response = $this->_sendCommand('BODY '.$id); |
||
| 763 | if (self::isError($response)) { |
||
| 764 | return $response; |
||
| 765 | } |
||
| 766 | if ($response !== NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) { |
||
| 767 | return $this->_handleErrorResponse($response); |
||
| 768 | } |
||
| 769 | |||
| 770 | // Use array to accumulate lines (faster than string concatenation) |
||
| 771 | $bodyParts = []; |
||
| 772 | $socket = $this->_socket; |
||
| 773 | |||
| 774 | while (! feof($socket)) { |
||
| 775 | $line = fgets($socket, 8192); |
||
| 776 | if ($line === false) { |
||
| 777 | return $this->throwError('Failed to read line from socket.', null); |
||
| 778 | } |
||
| 779 | if ($line === ".\r\n") { |
||
| 780 | $body = implode('', $bodyParts); |
||
| 781 | |||
| 782 | return $this->_yencService->decodeIgnore($body); |
||
| 783 | } |
||
| 784 | if ($line[0] === '.' && isset($line[1]) && $line[1] === '.') { |
||
| 785 | $line = substr($line, 1); |
||
| 786 | } |
||
| 787 | $bodyParts[] = $line; |
||
| 788 | } |
||
| 789 | |||
| 790 | return $this->throwError('End of stream! Connection lost?', null); |
||
| 791 | } |
||
| 792 | |||
| 793 | /** |
||
| 794 | * Restart the NNTP connection if an error occurs in the selectGroup |
||
| 795 | * function, if it does not restart display the error. |
||
| 796 | * |
||
| 797 | * @param NNTPService $nntp Instance of class NNTPService. |
||
| 798 | * @param string $group Name of the group. |
||
| 799 | * @param bool $comp Use compression or not? |
||
| 800 | * @return mixed On success : (array) The group summary. |
||
| 801 | * |
||
| 802 | * @throws \Exception |
||
| 803 | * On Failure : (object) PEAR_Error. |
||
| 804 | */ |
||
| 805 | public function dataError(NNTPService $nntp, string $group, bool $comp = true): mixed |
||
| 806 | { |
||
| 807 | // Disconnect. |
||
| 808 | $nntp->doQuit(); |
||
| 809 | // Try reconnecting. This uses another round of max retries. |
||
| 810 | if ($nntp->doConnect($comp) !== true) { |
||
| 811 | return $this->throwError('Unable to reconnect to usenet!'); |
||
| 812 | } |
||
| 813 | |||
| 814 | // Try re-selecting the group. |
||
| 815 | $data = $nntp->selectGroup($group); |
||
| 816 | if (self::isError($data)) { |
||
| 817 | $message = "Code {$data->code}: {$data->message}\nSkipping group: {$group}"; |
||
| 818 | |||
| 819 | if ($this->_echo) { |
||
| 820 | cli()->error($message); |
||
| 821 | } |
||
| 822 | $nntp->doQuit(); |
||
| 823 | } |
||
| 824 | |||
| 825 | return $data; |
||
| 826 | } |
||
| 827 | |||
| 828 | /** |
||
| 829 | * Split a string into lines of 510 chars ending with \r\n. |
||
| 830 | * Usenet limits lines to 512 chars, with \r\n that leaves us 510. |
||
| 831 | * |
||
| 832 | * @param string $string The string to split. |
||
| 833 | * @param bool $compress Compress the string with gzip? |
||
| 834 | * @return string The split string. |
||
| 835 | */ |
||
| 836 | protected function _splitLines(string $string, bool $compress = false): string |
||
| 837 | { |
||
| 838 | // Check if the length is longer than 510 chars. |
||
| 839 | if (\strlen($string) > 510) { |
||
| 840 | // If it is, split it @ 510 and terminate with \r\n. |
||
| 841 | $string = chunk_split($string, 510, "\r\n"); |
||
| 842 | } |
||
| 843 | |||
| 844 | // Compress the string if requested. |
||
| 845 | return $compress ? gzdeflate($string, 4) : $string; |
||
| 846 | } |
||
| 847 | |||
| 848 | /** |
||
| 849 | * Try to see if the NNTP server implements XFeature GZip Compression, |
||
| 850 | * change the compression bool object if so. |
||
| 851 | * |
||
| 852 | * @param bool $secondTry This is only used if enabling compression fails, the function will call itself to retry. |
||
| 853 | * @return mixed On success : (bool) True: The server understood and compression is enabled. |
||
| 854 | * (bool) False: The server did not understand, compression is not enabled. |
||
| 855 | * On failure : (object) PEAR_Error. |
||
| 856 | * |
||
| 857 | * @throws \Exception |
||
| 858 | */ |
||
| 859 | protected function _enableCompression(bool $secondTry = false): mixed |
||
| 860 | { |
||
| 861 | if ($this->_compressionEnabled) { |
||
| 862 | return true; |
||
| 863 | } |
||
| 864 | if (! $this->_compressionSupported) { |
||
| 865 | return false; |
||
| 866 | } |
||
| 867 | |||
| 868 | // Send this command to the usenet server. |
||
| 869 | $response = $this->_sendCommand('XFEATURE COMPRESS GZIP'); |
||
| 870 | |||
| 871 | // Check if it's good. |
||
| 872 | if (self::isError($response)) { |
||
| 873 | $this->_compressionSupported = false; |
||
| 874 | |||
| 875 | return $response; |
||
| 876 | } |
||
| 877 | if ($response !== 290) { |
||
| 878 | if (! $secondTry) { |
||
| 879 | // Retry. |
||
| 880 | $this->cmdQuit(); |
||
| 881 | if ($this->_checkConnection()) { |
||
| 882 | return $this->_enableCompression(true); |
||
| 883 | } |
||
| 884 | } |
||
| 885 | $msg = "Sent 'XFEATURE COMPRESS GZIP' to server, got '$response: ".$this->_currentStatusResponse()."'"; |
||
| 886 | |||
| 887 | $this->_compressionSupported = false; |
||
| 888 | |||
| 889 | return false; |
||
| 890 | } |
||
| 891 | |||
| 892 | $this->_compressionEnabled = true; |
||
| 893 | $this->_compressionSupported = true; |
||
| 894 | |||
| 895 | return true; |
||
| 896 | } |
||
| 897 | |||
| 898 | /** |
||
| 899 | * Override PEAR NNTP's function to use our _getXFeatureTextResponse instead |
||
| 900 | * of their _getTextResponse function since it is incompatible at decoding |
||
| 901 | * headers when XFeature GZip compression is enabled server side. |
||
| 902 | * |
||
| 903 | * @return \App\Services\NNTP\NNTPService|array|string Our overridden function when compression is enabled. |
||
| 904 | * parent Parent function when no compression. |
||
| 905 | */ |
||
| 906 | public function _getTextResponse(): NNTPService|array|string |
||
| 907 | { |
||
| 908 | if ($this->_compressionEnabled && |
||
| 909 | isset($this->_currentStatusResponse[1]) && |
||
| 910 | stripos($this->_currentStatusResponse[1], 'COMPRESS=GZIP') !== false) { |
||
| 911 | return $this->_getXFeatureTextResponse(); |
||
| 912 | } |
||
| 913 | |||
| 914 | return parent::_getTextResponse(); |
||
| 915 | } |
||
| 916 | |||
| 917 | /** |
||
| 918 | * Loop over the compressed data when XFeature GZip Compress is turned on, |
||
| 919 | * string the data until we find a indicator |
||
| 920 | * (period, carriage feed, line return ;; .\r\n), decompress the data, |
||
| 921 | * split the data (bunch of headers in a string) into an array, finally |
||
| 922 | * return the array. |
||
| 923 | * |
||
| 924 | * Have we failed to decompress the data, was there a |
||
| 925 | * problem downloading the data, etc.. |
||
| 926 | * |
||
| 927 | * @return array|string On success : (array) The headers. |
||
| 928 | * On failure : (object) PEAR_Error. |
||
| 929 | * On decompress failure: (string) error message |
||
| 930 | */ |
||
| 931 | protected function &_getXFeatureTextResponse(): array|string |
||
| 932 | { |
||
| 933 | $possibleTerm = false; |
||
| 934 | // Use array accumulation for better performance with large data |
||
| 935 | $dataParts = []; |
||
| 936 | $socket = $this->_socket; |
||
| 937 | |||
| 938 | while (! feof($socket)) { |
||
| 939 | // Did we find a possible ending ? (.\r\n) |
||
| 940 | if ($possibleTerm) { |
||
| 941 | // Use stream_select for more efficient socket polling |
||
| 942 | $read = [$socket]; |
||
| 943 | $write = $except = null; |
||
| 944 | |||
| 945 | // Check if data is available with a short timeout (5ms) |
||
| 946 | $ready = @stream_select($read, $write, $except, 0, 5000); |
||
| 947 | |||
| 948 | if ($ready > 0) { |
||
| 949 | // Data available, read it |
||
| 950 | stream_set_blocking($socket, false); |
||
| 951 | $buffer = fgets($socket, 16384); |
||
| 952 | stream_set_blocking($socket, true); |
||
| 953 | } else { |
||
| 954 | $buffer = ''; |
||
| 955 | } |
||
| 956 | |||
| 957 | // If the buffer was really empty, then we know $possibleTerm was the real ending. |
||
| 958 | if ($buffer === '' || $buffer === false) { |
||
| 959 | // Join all parts and remove .\r\n from end, decompress data. |
||
| 960 | $data = implode('', $dataParts); |
||
| 961 | $deComp = @gzuncompress(substr($data, 0, -3)); |
||
| 962 | |||
| 963 | if (! empty($deComp)) { |
||
| 964 | $bytesReceived = \strlen($data); |
||
| 965 | if ($this->_echo && $bytesReceived > 10240) { |
||
| 966 | cli()->primaryOver( |
||
| 967 | 'Received '.round($bytesReceived / 1024). |
||
| 968 | 'KB from group ('.$this->group().').' |
||
| 969 | ); |
||
| 970 | } |
||
| 971 | |||
| 972 | // Split the string of headers into an array of individual headers, then return it. |
||
| 973 | $deComp = explode("\r\n", trim($deComp)); |
||
| 974 | |||
| 975 | return $deComp; |
||
| 976 | } |
||
| 977 | $message = 'Decompression of OVER headers failed.'; |
||
| 978 | |||
| 979 | return $this->throwError(cli()->error($message), 1000); |
||
| 980 | } |
||
| 981 | // The buffer was not empty, so we know this was not the real ending, so reset $possibleTerm. |
||
| 982 | $possibleTerm = false; |
||
| 983 | $dataParts[] = $buffer; |
||
| 984 | } else { |
||
| 985 | // Get data from the stream with larger buffer. |
||
| 986 | $buffer = fgets($socket, 16384); |
||
| 987 | } |
||
| 988 | |||
| 989 | // If we got no data at all try one more time to pull data. |
||
| 990 | if (empty($buffer)) { |
||
| 991 | usleep(5000); |
||
| 992 | $buffer = fgets($socket, 16384); |
||
| 993 | |||
| 994 | // If we got nothing again, return error. |
||
| 995 | if (empty($buffer)) { |
||
| 996 | $message = 'Error fetching data from usenet server while downloading OVER headers.'; |
||
| 997 | |||
| 998 | return $this->throwError(cli()->error($message), 1000); |
||
| 999 | } |
||
| 1000 | } |
||
| 1001 | |||
| 1002 | // Append current buffer to parts array. |
||
| 1003 | $dataParts[] = $buffer; |
||
| 1004 | |||
| 1005 | // Check if we have the ending (.\r\n) - check last 3 chars directly |
||
| 1006 | $bufLen = \strlen($buffer); |
||
| 1007 | if ($bufLen >= 3 && $buffer[$bufLen - 3] === '.' && $buffer[$bufLen - 2] === "\r" && $buffer[$bufLen - 1] === "\n") { |
||
| 1008 | // We have a possible ending, next loop check if it is. |
||
| 1009 | $possibleTerm = true; |
||
| 1010 | } |
||
| 1011 | } |
||
| 1012 | |||
| 1013 | $message = 'Unspecified error while downloading OVER headers.'; |
||
| 1014 | |||
| 1015 | return $this->throwError(cli()->error($message), 1000); |
||
| 1016 | } |
||
| 1017 | |||
| 1018 | /** |
||
| 1019 | * Check if the Message-ID has the required opening and closing brackets. |
||
| 1020 | * |
||
| 1021 | * @param string $messageID The Message-ID with or without brackets. |
||
| 1022 | * @return string|false Message-ID with brackets or false if empty. |
||
| 1023 | */ |
||
| 1024 | protected function _formatMessageID(string $messageID): string|false |
||
| 1025 | { |
||
| 1026 | $messageID = (string) $messageID; |
||
| 1027 | if ($messageID === '') { |
||
| 1028 | return false; |
||
| 1029 | } |
||
| 1030 | |||
| 1031 | // Check if the first char is <, if not add it. |
||
| 1032 | if ($messageID[0] !== '<') { |
||
| 1033 | $messageID = ('<'.$messageID); |
||
| 1034 | } |
||
| 1035 | |||
| 1036 | // Check if the last char is >, if not add it. |
||
| 1037 | if (! str_ends_with($messageID, '>')) { |
||
| 1038 | $messageID .= '>'; |
||
| 1039 | } |
||
| 1040 | |||
| 1041 | return $messageID; |
||
| 1042 | } |
||
| 1043 | |||
| 1044 | /** |
||
| 1045 | * Download an article body (an article without the header). |
||
| 1046 | * |
||
| 1047 | * @return mixed|object|string |
||
| 1048 | * |
||
| 1049 | * @throws \Exception |
||
| 1050 | */ |
||
| 1051 | protected function _getMessage(string $groupName, mixed $identifier): mixed |
||
| 1052 | { |
||
| 1053 | // Make sure the requested group is already selected, if not select it. |
||
| 1054 | if ($this->group() !== $groupName) { |
||
| 1055 | // Select the group. |
||
| 1056 | $summary = $this->selectGroup($groupName); |
||
| 1057 | // If there was an error selecting the group, return a PEAR error object. |
||
| 1058 | if (self::isError($summary)) { |
||
| 1059 | return $summary; |
||
| 1060 | } |
||
| 1061 | } |
||
| 1062 | |||
| 1063 | // Check if this is an article number or message-id. |
||
| 1064 | if (! is_numeric($identifier)) { |
||
| 1065 | // It's a message-id so check if it has the triangular brackets. |
||
| 1066 | $identifier = $this->_formatMessageID($identifier); |
||
| 1067 | } |
||
| 1068 | |||
| 1069 | // Tell the news server we want the body of an article. |
||
| 1070 | $response = $this->_sendCommand('BODY '.$identifier); |
||
| 1071 | if (self::isError($response)) { |
||
| 1072 | return $response; |
||
| 1073 | } |
||
| 1074 | |||
| 1075 | if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) { |
||
| 1076 | // Use array to accumulate lines (faster than string concatenation for many appends) |
||
| 1077 | $bodyParts = []; |
||
| 1078 | $socket = $this->_socket; |
||
| 1079 | |||
| 1080 | // Continue until connection is lost |
||
| 1081 | while (! feof($socket)) { |
||
| 1082 | // Retrieve and append up to 8192 characters from the server (larger buffer = fewer syscalls) |
||
| 1083 | $line = fgets($socket, 8192); |
||
| 1084 | |||
| 1085 | // If the socket is empty/ an error occurs, false is returned. |
||
| 1086 | if ($line === false) { |
||
| 1087 | return $this->throwError('Failed to read line from socket.', null); |
||
| 1088 | } |
||
| 1089 | |||
| 1090 | // Check if the line terminates the text response. |
||
| 1091 | if ($line === ".\r\n") { |
||
| 1092 | // Join all parts and attempt to yEnc decode |
||
| 1093 | $body = implode('', $bodyParts); |
||
| 1094 | |||
| 1095 | return $this->_yencService->decodeIgnore($body); |
||
| 1096 | } |
||
| 1097 | |||
| 1098 | // Check for line that starts with double period, remove one. |
||
| 1099 | if ($line[0] === '.' && isset($line[1]) && $line[1] === '.') { |
||
| 1100 | $line = substr($line, 1); |
||
| 1101 | } |
||
| 1102 | |||
| 1103 | // Add the line to the array |
||
| 1104 | $bodyParts[] = $line; |
||
| 1105 | } |
||
| 1106 | |||
| 1107 | return $this->throwError('End of stream! Connection lost?', null); |
||
| 1108 | } |
||
| 1109 | |||
| 1110 | return $this->_handleErrorResponse($response); |
||
| 1111 | } |
||
| 1112 | |||
| 1113 | /** |
||
| 1114 | * Check if we are still connected. Reconnect if not. |
||
| 1115 | * |
||
| 1116 | * @param bool $reSelectGroup Select back the group after connecting? |
||
| 1117 | * @return mixed On success: (bool) True; |
||
| 1118 | * |
||
| 1119 | * @throws \Exception |
||
| 1120 | * On failure: (object) PEAR_Error |
||
| 1121 | */ |
||
| 1122 | protected function _checkConnection(bool $reSelectGroup = true): mixed |
||
| 1123 | { |
||
| 1124 | $currentGroup = $this->_currentGroup; |
||
| 1125 | // Check if we are connected. |
||
| 1126 | if (parent::_isConnected()) { |
||
| 1127 | $retVal = true; |
||
| 1128 | } else { |
||
| 1129 | switch ($this->_currentServer) { |
||
| 1130 | case $this->_configServer: |
||
| 1131 | if (\is_resource($this->_socket)) { |
||
| 1132 | $this->doQuit(true); |
||
| 1133 | } |
||
| 1134 | $retVal = $this->doConnect(); |
||
| 1135 | break; |
||
| 1136 | case $this->_configAlternateServer: |
||
| 1137 | if (\is_resource($this->_socket)) { |
||
| 1138 | $this->doQuit(true); |
||
| 1139 | } |
||
| 1140 | $retVal = $this->doConnect(true, true); |
||
| 1141 | break; |
||
| 1142 | default: |
||
| 1143 | $retVal = $this->throwError('Wrong server constant used in NNTP checkConnection()!'); |
||
| 1144 | } |
||
| 1145 | |||
| 1146 | if ($retVal === true && $reSelectGroup) { |
||
| 1147 | $group = $this->selectGroup($currentGroup); |
||
| 1148 | if (self::isError($group)) { |
||
| 1149 | $retVal = $group; |
||
| 1150 | } |
||
| 1151 | } |
||
| 1152 | } |
||
| 1153 | |||
| 1154 | return $retVal; |
||
| 1155 | } |
||
| 1156 | |||
| 1157 | /** |
||
| 1158 | * Verify NNTP error code and return PEAR error. |
||
| 1159 | * |
||
| 1160 | * @param int $response NET_NNTP Response code |
||
| 1161 | * @return object PEAR error |
||
| 1162 | */ |
||
| 1163 | protected function _handleErrorResponse(int $response): object |
||
| 1164 | { |
||
| 1165 | switch ($response) { |
||
| 1166 | // 381, RFC2980: 'More authentication information required' |
||
| 1167 | case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_CONTINUE: |
||
| 1168 | return $this->throwError('More authentication information required', $response, $this->_currentStatusResponse()); |
||
| 1169 | // 400, RFC977: 'Service discontinued' |
||
| 1170 | case NET_NNTP_PROTOCOL_RESPONSECODE_DISCONNECTING_FORCED: |
||
| 1171 | return $this->throwError('Server refused connection', $response, $this->_currentStatusResponse()); |
||
| 1172 | // 411, RFC977: 'no such news group' |
||
| 1173 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_GROUP: |
||
| 1174 | return $this->throwError('No such news group on server', $response, $this->_currentStatusResponse()); |
||
| 1175 | // 412, RFC2980: 'No news group current selected' |
||
| 1176 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: |
||
| 1177 | return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse()); |
||
| 1178 | // 420, RFC2980: 'Current article number is invalid' |
||
| 1179 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: |
||
| 1180 | return $this->throwError('Current article number is invalid', $response, $this->_currentStatusResponse()); |
||
| 1181 | // 421, RFC977: 'no next article in this group' |
||
| 1182 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_NEXT_ARTICLE: |
||
| 1183 | return $this->throwError('No next article in this group', $response, $this->_currentStatusResponse()); |
||
| 1184 | // 422, RFC977: 'no previous article in this group' |
||
| 1185 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_PREVIOUS_ARTICLE: |
||
| 1186 | return $this->throwError('No previous article in this group', $response, $this->_currentStatusResponse()); |
||
| 1187 | // 423, RFC977: 'No such article number in this group' |
||
| 1188 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: |
||
| 1189 | return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse()); |
||
| 1190 | // 430, RFC977: 'No such article found' |
||
| 1191 | case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID: |
||
| 1192 | return $this->throwError('No such article found', $response, $this->_currentStatusResponse()); |
||
| 1193 | // 435, RFC977: 'Article not wanted' |
||
| 1194 | case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_UNWANTED: |
||
| 1195 | return $this->throwError('Article not wanted', $response, $this->_currentStatusResponse()); |
||
| 1196 | // 436, RFC977: 'Transfer failed - try again later' |
||
| 1197 | case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_FAILURE: |
||
| 1198 | return $this->throwError('Transfer failed - try again later', $response, $this->_currentStatusResponse()); |
||
| 1199 | // 437, RFC977: 'Article rejected - do not try again' |
||
| 1200 | case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_REJECTED: |
||
| 1201 | return $this->throwError('Article rejected - do not try again', $response, $this->_currentStatusResponse()); |
||
| 1202 | // 440, RFC977: 'posting not allowed' |
||
| 1203 | case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_PROHIBITED: |
||
| 1204 | return $this->throwError('Posting not allowed', $response, $this->_currentStatusResponse()); |
||
| 1205 | // 441, RFC977: 'posting failed' |
||
| 1206 | case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_FAILURE: |
||
| 1207 | return $this->throwError('Posting failed', $response, $this->_currentStatusResponse()); |
||
| 1208 | // 481, RFC2980: 'Groups and descriptions unavailable' |
||
| 1209 | case NET_NNTP_PROTOCOL_RESPONSECODE_XGTITLE_GROUPS_UNAVAILABLE: |
||
| 1210 | return $this->throwError('Groups and descriptions unavailable', $response, $this->_currentStatusResponse()); |
||
| 1211 | // 482, RFC2980: 'Authentication rejected' |
||
| 1212 | case NET_NNTP_PROTOCOL_RESPONSECODE_AUTHENTICATION_REJECTED: |
||
| 1213 | return $this->throwError('Authentication rejected', $response, $this->_currentStatusResponse()); |
||
| 1214 | // 500, RFC977: 'Command not recognized' |
||
| 1215 | case NET_NNTP_PROTOCOL_RESPONSECODE_UNKNOWN_COMMAND: |
||
| 1216 | return $this->throwError('Command not recognized', $response, $this->_currentStatusResponse()); |
||
| 1217 | // 501, RFC977: 'Command syntax error' |
||
| 1218 | case NET_NNTP_PROTOCOL_RESPONSECODE_SYNTAX_ERROR: |
||
| 1219 | return $this->throwError('Command syntax error', $response, $this->_currentStatusResponse()); |
||
| 1220 | // 502, RFC2980: 'No permission' |
||
| 1221 | case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_PERMITTED: |
||
| 1222 | return $this->throwError('No permission', $response, $this->_currentStatusResponse()); |
||
| 1223 | // 503, RFC2980: 'Program fault - command not performed' |
||
| 1224 | case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_SUPPORTED: |
||
| 1225 | return $this->throwError('Internal server error, function not performed', $response, $this->_currentStatusResponse()); |
||
| 1226 | // RFC4642: 'Can not initiate TLS negotiation' |
||
| 1227 | case NET_NNTP_PROTOCOL_RESPONSECODE_TLS_FAILED_NEGOTIATION: |
||
| 1228 | return $this->throwError('Can not initiate TLS negotiation', $response, $this->_currentStatusResponse()); |
||
| 1229 | default: |
||
| 1230 | $text = $this->_currentStatusResponse(); |
||
| 1231 | |||
| 1232 | return $this->throwError("Unexpected response: '$text'", $response, $text); |
||
| 1233 | } |
||
| 1234 | } |
||
| 1235 | |||
| 1236 | /** |
||
| 1237 | * Connect to a NNTP server. |
||
| 1238 | * |
||
| 1239 | * @param string|null $host (optional) The address of the NNTP-server to connect to, defaults to 'localhost'. |
||
| 1240 | * @param mixed|null $encryption (optional) Use TLS/SSL on the connection? |
||
| 1241 | * (string) 'tcp' => Use no encryption. |
||
| 1242 | * 'ssl', 'sslv3', 'tls' => Use encryption. |
||
| 1243 | * (null)|(false) Use no encryption. |
||
| 1244 | * @param int|null $port (optional) The port number to connect to, defaults to 119. |
||
| 1245 | * @param int|null $timeout (optional) How many seconds to wait before giving up when connecting. |
||
| 1246 | * @param int $socketTimeout (optional) How many seconds to wait before timing out the (blocked) socket. |
||
| 1247 | * @return mixed (bool) On success: True when posting allowed, otherwise false. |
||
| 1248 | * (object) On failure: pear_error |
||
| 1249 | */ |
||
| 1250 | public function connect(?string $host = null, mixed $encryption = null, ?int $port = null, ?int $timeout = 15, int $socketTimeout = 120): mixed |
||
| 1251 | { |
||
| 1252 | if ($this->_isConnected()) { |
||
| 1253 | return $this->throwError('Already connected, disconnect first!', null); |
||
| 1254 | } |
||
| 1255 | // v1.0.x API |
||
| 1256 | if (is_int($encryption)) { |
||
| 1257 | trigger_error('You are using deprecated API v1.0 in Net_NNTP_Protocol_Client: connect() !', E_USER_NOTICE); |
||
| 1258 | $port = $encryption; |
||
| 1259 | $encryption = false; |
||
| 1260 | } |
||
| 1261 | if ($host === null) { |
||
| 1262 | $host = 'localhost'; |
||
| 1263 | } |
||
| 1264 | // Choose transport based on encryption, and if no port is given, use default for that encryption. |
||
| 1265 | switch ($encryption) { |
||
| 1266 | case null: |
||
| 1267 | case 'tcp': |
||
| 1268 | $transport = 'tcp'; |
||
| 1269 | $port = $port ?? 119; |
||
| 1270 | break; |
||
| 1271 | case 'ssl': |
||
| 1272 | case 'tls': |
||
| 1273 | $transport = $encryption; |
||
| 1274 | $port = $port ?? 563; |
||
| 1275 | break; |
||
| 1276 | default: |
||
| 1277 | $message = '$encryption parameter must be either tcp, tls, ssl.'; |
||
| 1278 | trigger_error($message, E_USER_ERROR); |
||
| 1279 | } |
||
| 1280 | // Attempt to connect to usenet. |
||
| 1281 | // Only create SSL context if using TLS/SSL transport |
||
| 1282 | $context = preg_match('/tls|ssl/', $transport) |
||
| 1283 | ? stream_context_create(streamSslContextOptions()) |
||
| 1284 | : null; |
||
| 1285 | |||
| 1286 | $socket = stream_socket_client( |
||
| 1287 | $transport.'://'.$host.':'.$port, |
||
| 1288 | $errorNumber, |
||
| 1289 | $errorString, |
||
| 1290 | $timeout, |
||
| 1291 | STREAM_CLIENT_CONNECT, |
||
| 1292 | $context |
||
| 1293 | ); |
||
| 1294 | if ($socket === false) { |
||
| 1295 | $message = "Connection to $transport://$host:$port failed."; |
||
| 1296 | if (preg_match('/tls|ssl/', $transport)) { |
||
| 1297 | $message .= ' Try disabling SSL/TLS, and/or try a different port.'; |
||
| 1298 | } |
||
| 1299 | $message .= ' [ERROR '.$errorNumber.': '.$errorString.']'; |
||
| 1300 | |||
| 1301 | return $this->throwError($message); |
||
| 1302 | } |
||
| 1303 | // Store the socket resource as property. |
||
| 1304 | $this->_socket = $socket; |
||
| 1305 | $this->_socketTimeout = $socketTimeout ?: $this->_socketTimeout; |
||
| 1306 | // Set the socket timeout. |
||
| 1307 | stream_set_timeout($this->_socket, $this->_socketTimeout); |
||
| 1308 | // Retrieve the server's initial response. |
||
| 1309 | $response = $this->_getStatusResponse(); |
||
| 1310 | if (self::isError($response)) { |
||
| 1311 | return $response; |
||
| 1312 | } |
||
| 1313 | switch ($response) { |
||
| 1314 | // 200, Posting allowed |
||
| 1315 | case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_ALLOWED: |
||
| 1316 | return true; |
||
| 1317 | // 201, Posting NOT allowed |
||
| 1318 | case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_PROHIBITED: |
||
| 1319 | |||
| 1320 | return false; |
||
| 1321 | default: |
||
| 1322 | return $this->_handleErrorResponse($response); |
||
| 1323 | } |
||
| 1324 | } |
||
| 1325 | |||
| 1326 | /** |
||
| 1327 | * Test whether we are connected or not. |
||
| 1328 | * |
||
| 1329 | * @param bool $feOf Check for the end of file pointer. |
||
| 1330 | * @return bool true or false |
||
| 1331 | */ |
||
| 1332 | public function _isConnected(bool $feOf = true): bool |
||
| 1333 | { |
||
| 1334 | return is_resource($this->_socket) && (! $feOf || ! feof($this->_socket)); |
||
| 1335 | } |
||
| 1336 | |||
| 1337 | /** |
||
| 1338 | * Check if we are connected to the usenet server. |
||
| 1339 | */ |
||
| 1340 | public function isConnected(): bool |
||
| 1341 | { |
||
| 1342 | return $this->_isConnected(); |
||
| 1343 | } |
||
| 1344 | |||
| 1345 | /** |
||
| 1346 | * Get the current server address. |
||
| 1347 | */ |
||
| 1348 | public function getCurrentServer(): string |
||
| 1349 | { |
||
| 1350 | return $this->_currentServer; |
||
| 1351 | } |
||
| 1352 | |||
| 1353 | /** |
||
| 1354 | * Get the current port. |
||
| 1355 | */ |
||
| 1356 | public function getCurrentPort(): int|string |
||
| 1357 | { |
||
| 1358 | return $this->_currentPort; |
||
| 1359 | } |
||
| 1360 | |||
| 1361 | /** |
||
| 1362 | * Check if posting is allowed on the current connection. |
||
| 1363 | */ |
||
| 1364 | public function isPostingAllowed(): bool |
||
| 1365 | { |
||
| 1366 | return $this->_postingAllowed; |
||
| 1367 | } |
||
| 1368 | |||
| 1369 | /** |
||
| 1370 | * Get the currently selected group. |
||
| 1371 | */ |
||
| 1372 | public function getCurrentGroup(): string |
||
| 1375 | } |
||
| 1376 | |||
| 1377 | /** |
||
| 1378 | * Check if compression is enabled. |
||
| 1379 | */ |
||
| 1380 | public function isCompressionEnabled(): bool |
||
| 1381 | { |
||
| 1382 | return $this->_compressionEnabled; |
||
| 1383 | } |
||
| 1384 | |||
| 1385 | /** |
||
| 1386 | * Check if compression is supported. |
||
| 1387 | */ |
||
| 1388 | public function isCompressionSupported(): bool |
||
| 1389 | { |
||
| 1390 | return $this->_compressionSupported; |
||
| 1391 | } |
||
| 1392 | } |
||
| 1393 | |||
| 1394 |