diff --git a/Blacklight/NNTP.php b/Blacklight/NNTP.php index 022d50a71..062ebd61b 100755 --- a/Blacklight/NNTP.php +++ b/Blacklight/NNTP.php @@ -108,6 +108,23 @@ class NNTP extends \Net_NNTP_Client */ protected $_socket = null; + /** + * Cached config values for performance. + */ + protected string $_configServer; + protected string $_configAlternateServer; + protected int $_configPort; + protected int $_configAlternatePort; + protected bool $_configSsl; + protected bool $_configAlternateSsl; + protected string $_configUsername; + protected string $_configPassword; + protected string $_configAlternateUsername; + protected string $_configAlternatePassword; + protected int $_configSocketTimeout; + protected int $_configAlternateSocketTimeout; + protected bool $_configCompressedHeaders; + /** * Default constructor. */ @@ -119,8 +136,24 @@ class NNTP extends \Net_NNTP_Client $this->_tmux = new Tmux; $this->_nntpRetries = Settings::settingValue('nntpretries') !== '' ? (int) Settings::settingValue('nntpretries') : 0 + 1; $this->colorCli = new ColorCLI; - $this->_currentPort = config('nntmux_nntp.port'); - $this->_currentServer = config('nntmux_nntp.server'); + + // Cache config values to avoid repeated lookups + $this->_configServer = config('nntmux_nntp.server'); + $this->_configAlternateServer = config('nntmux_nntp.alternate_server'); + $this->_configPort = (int) config('nntmux_nntp.port'); + $this->_configAlternatePort = (int) config('nntmux_nntp.alternate_server_port'); + $this->_configSsl = (bool) config('nntmux_nntp.ssl'); + $this->_configAlternateSsl = (bool) config('nntmux_nntp.alternate_server_ssl'); + $this->_configUsername = config('nntmux_nntp.username') ?? ''; + $this->_configPassword = config('nntmux_nntp.password') ?? ''; + $this->_configAlternateUsername = config('nntmux_nntp.alternate_server_username') ?? ''; + $this->_configAlternatePassword = config('nntmux_nntp.alternate_server_password') ?? ''; + $this->_configSocketTimeout = (int) (config('nntmux_nntp.socket_timeout') ?: $this->_socketTimeout); + $this->_configAlternateSocketTimeout = (int) (config('nntmux_nntp.alternate_server_socket_timeout') ?: $this->_socketTimeout); + $this->_configCompressedHeaders = (bool) config('nntmux_nntp.compressed_headers'); + + $this->_currentPort = $this->_configPort; + $this->_currentServer = $this->_configServer; $this->_primaryNntpConnections = config('nntmux_nntp.main_nntp_connections'); $this->_alternateNntpConnections = config('nntmux_nntp.alternate_nntp_connections'); $this->_selectedGroupSummary = null; @@ -149,16 +182,16 @@ class NNTP extends \Net_NNTP_Client public function doConnect(bool $compression = true, bool $alternate = false): mixed { $primaryUSP = [ - 'ip' => gethostbyname(config('nntmux_nntp.server')), - 'port' => config('nntmux_nntp.port'), + 'ip' => gethostbyname($this->_configServer), + 'port' => $this->_configPort, ]; $alternateUSP = [ - 'ip_a' => gethostbyname(config('nntmux_nntp.alternate_server')), - 'port_a' => config('nntmux_nntp.alternate_server_port'), + 'ip_a' => gethostbyname($this->_configAlternateServer), + 'port_a' => $this->_configAlternatePort, ]; $primaryConnections = $this->_tmux->getUSPConnections('primary', $primaryUSP); $alternateConnections = $this->_tmux->getUSPConnections('alternate', $alternateUSP); - if ($this->_isConnected() && (($alternate && $this->_currentServer === config('nntmux_nntp.alternate_server') && ($this->_primaryNntpConnections < $alternateConnections['alternate']['active'])) || (! $alternate && $this->_currentServer === config('nntmux_nntp.server') && ($this->_primaryNntpConnections < $primaryConnections['primary']['active'])))) { + if ($this->_isConnected() && (($alternate && $this->_currentServer === $this->_configAlternateServer && ($this->_primaryNntpConnections < $alternateConnections['alternate']['active'])) || (! $alternate && $this->_currentServer === $this->_configServer && ($this->_primaryNntpConnections < $primaryConnections['primary']['active'])))) { return true; } @@ -168,19 +201,19 @@ class NNTP extends \Net_NNTP_Client // Set variables to connect based on if we are using the alternate provider or not. if (! $alternate) { - $sslEnabled = (bool) config('nntmux_nntp.ssl'); - $this->_currentServer = config('nntmux_nntp.server'); - $this->_currentPort = config('nntmux_nntp.port'); - $userName = config('nntmux_nntp.username'); - $password = config('nntmux_nntp.password'); - $socketTimeout = ! empty(config('nntmux_nntp.socket_timeout')) ? config('nntmux_nntp.socket_timeout') : $this->_socketTimeout; + $sslEnabled = $this->_configSsl; + $this->_currentServer = $this->_configServer; + $this->_currentPort = $this->_configPort; + $userName = $this->_configUsername; + $password = $this->_configPassword; + $socketTimeout = $this->_configSocketTimeout; } else { - $sslEnabled = (bool) config('nntmux_nntp.alternate_server_ssl'); - $this->_currentServer = config('nntmux_nntp.alternate_server'); - $this->_currentPort = config('nntmux_nntp.alternate_server_port'); - $userName = config('nntmux_nntp.alternate_server_username'); - $password = config('nntmux_nntp.alternate_server_password'); - $socketTimeout = ! empty(config('nntmux_nntp.alternate_server_socket_timeout')) ? config('nntmux_nntp.alternate_server_socket_timeout') : $this->_socketTimeout; + $sslEnabled = $this->_configAlternateSsl; + $this->_currentServer = $this->_configAlternateServer; + $this->_currentPort = $this->_configAlternatePort; + $userName = $this->_configAlternateUsername; + $password = $this->_configAlternatePassword; + $socketTimeout = $this->_configAlternateSocketTimeout; } $enc = ($sslEnabled ? ' (ssl)' : ' (non-ssl)'); @@ -268,7 +301,7 @@ class NNTP extends \Net_NNTP_Client // If we are connected and authenticated, try enabling compression if we have it enabled. if ($connected && $authenticated) { // Check if we should use compression on the connection. - if (! $compression || config('nntmux_nntp.compressed_headers') === false) { + if (! $compression || ! $this->_configCompressedHeaders) { $this->_compressionSupported = false; } @@ -333,7 +366,7 @@ class NNTP extends \Net_NNTP_Client */ public function enableCompression(): void { - if (config('nntmux_nntp.compressed_headers') === false) { + if (! $this->_configCompressedHeaders) { return; } $this->_enableCompression(); @@ -451,36 +484,37 @@ class NNTP extends \Net_NNTP_Client } $this->_overviewFormatCache = $overview; } - // Add the "Number" key. - $overview = array_merge(['Number' => false], $overview); - // Iterator used for selecting the header elements to insert into the overview format array. - $iterator = 0; + // Pre-compute keys array and Xref position for faster processing + $keys = array_merge(['Number'], array_keys($overview)); + $keyCount = \count($keys); + $xrefIndex = array_search('Xref', $keys, true); // Loop over strings of headers. foreach ($data as $key => $header) { // Split the individual headers by tab. - $header = explode("\t", $header); + $parts = explode("\t", $header); // Make sure it's not empty. - if ($header === false) { + if ($parts === false || empty($parts)) { continue; } - // Temp array to store the header. - $headerArray = $overview; + // Build header array using pre-computed keys + $headerArray = []; + $partCount = \count($parts); - // Loop over the overview format and insert the individual header elements. - foreach ($overview as $name => $element) { - // Strip Xref: - if ($element === true) { - $header[$iterator] = substr($header[$iterator], 6); + for ($i = 0; $i < $keyCount && $i < $partCount; $i++) { + $value = $parts[$i]; + // Strip "Xref: " prefix if this is the Xref field + if ($i === $xrefIndex && isset($value[5])) { + $value = substr($value, 6); } - $headerArray[$name] = $header[$iterator++]; + $headerArray[$keys[$i]] = $value; } + // Add the individual header array back to the return array. $data[$key] = $headerArray; - $iterator = 0; } // Return the array of headers. @@ -562,9 +596,10 @@ class NNTP extends \Net_NNTP_Client // If there is an error try the alternate provider or return the PEAR error. } elseif ($alternate) { if (! $aConnected) { - $compressedHeaders = config('nntmux_nntp.compressed_headers'); // Check if the current connected server is the alternate or not. - $aConnected = $this->_currentServer === config('nntmux_nntp.server') ? $nntp->doConnect($compressedHeaders, true) : $nntp->doConnect(); + $aConnected = $this->_currentServer === $this->_configServer + ? $nntp->doConnect($this->_configCompressedHeaders, true) + : $nntp->doConnect(); } // If we connected successfully to usenet try to download the article body. if ($aConnected === true) { @@ -599,8 +634,7 @@ class NNTP extends \Net_NNTP_Client } elseif (\is_string($identifiers) || is_numeric($identifiers)) { $body = $this->_getMessage($groupName, $identifiers); if ($alternate && self::isError($body)) { - $compressedHeaders = config('nntmux_nntp.compressed_headers'); - $nntp->doConnect($compressedHeaders, true); + $nntp->doConnect($this->_configCompressedHeaders, true); $body = $nntp->_getMessage($groupName, $identifiers); $aConnected = true; } @@ -661,9 +695,8 @@ class NNTP extends \Net_NNTP_Client // Primary failed, try alternate if requested if ($alternate) { if (! $aConnected) { - $compressed = config('nntmux_nntp.compressed_headers'); - $aConnected = $this->_currentServer === config('nntmux_nntp.server') - ? $alt->doConnect($compressed, true) + $aConnected = $this->_currentServer === $this->_configServer + ? $alt->doConnect($this->_configCompressedHeaders, true) : $alt->doConnect(); } if ($aConnected === true) { @@ -714,19 +747,24 @@ class NNTP extends \Net_NNTP_Client if ($response !== NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) { return $this->_handleErrorResponse($response); } - $body = ''; - while (! feof($this->_socket)) { - $line = fgets($this->_socket, 1024); + + // Use array to accumulate lines (faster than string concatenation) + $bodyParts = []; + $socket = $this->_socket; + + while (! feof($socket)) { + $line = fgets($socket, 8192); if ($line === false) { return $this->throwError('Failed to read line from socket.', null); } if ($line === ".\r\n") { - return \App\Extensions\util\PhpYenc::decodeIgnore($body); + $body = implode('', $bodyParts); + return PhpYenc::decodeIgnore($body); } - if (str_starts_with($line, '.') && isset($line[1]) && $line[1] === '.') { + if ($line[0] === '.' && isset($line[1]) && $line[1] === '.') { $line = substr($line, 1); } - $body .= $line; + $bodyParts[] = $line; } return $this->throwError('End of stream! Connection lost?', null); @@ -888,35 +926,34 @@ class NNTP extends \Net_NNTP_Client protected function &_getXFeatureTextResponse(): array|string { $possibleTerm = false; - $data = null; + // Use array accumulation for better performance with large data + $dataParts = []; + $socket = $this->_socket; - while (! feof($this->_socket)) { + while (! feof($socket)) { // Did we find a possible ending ? (.\r\n) if ($possibleTerm) { - // Loop, sleeping shortly, to allow the server time to upload data, if it has any. - for ($i = 0; $i < 3; $i++) { - // If the socket is really empty, fGets will get stuck here, so set the socket to non blocking in case. - stream_set_blocking($this->_socket, 0); + // Use stream_select for more efficient socket polling + $read = [$socket]; + $write = $except = null; - // Now try to download from the socket. - $buffer = fgets($this->_socket); + // Check if data is available with a short timeout (5ms) + $ready = @stream_select($read, $write, $except, 0, 5000); - // And set back the socket to blocking. - stream_set_blocking($this->_socket, 1); - - // Don't sleep on last iteration. - if (! empty($buffer)) { - break; - } - if ($i < 2) { - usleep(10000); - } + if ($ready > 0) { + // Data available, read it + stream_set_blocking($socket, false); + $buffer = fgets($socket, 16384); + stream_set_blocking($socket, true); + } else { + $buffer = ''; } // If the buffer was really empty, then we know $possibleTerm was the real ending. - if (empty($buffer)) { - // Remove .\r\n from end, decompress data. - $deComp = @gzuncompress(mb_substr($data, 0, -3, '8bit')); + if ($buffer === '' || $buffer === false) { + // Join all parts and remove .\r\n from end, decompress data. + $data = implode('', $dataParts); + $deComp = @gzuncompress(substr($data, 0, -3)); if (! empty($deComp)) { $bytesReceived = \strlen($data); @@ -938,17 +975,18 @@ class NNTP extends \Net_NNTP_Client } // The buffer was not empty, so we know this was not the real ending, so reset $possibleTerm. $possibleTerm = false; + $dataParts[] = $buffer; } else { - // Get data from the stream. - $buffer = fgets($this->_socket); + // Get data from the stream with larger buffer. + $buffer = fgets($socket, 16384); } // If we got no data at all try one more time to pull data. if (empty($buffer)) { - usleep(10000); - $buffer = fgets($this->_socket); + usleep(5000); + $buffer = fgets($socket, 16384); - // If wet got nothing again, return error. + // If we got nothing again, return error. if (empty($buffer)) { $message = 'Error fetching data from usenet server while downloading OVER headers.'; @@ -956,11 +994,12 @@ class NNTP extends \Net_NNTP_Client } } - // Append current buffer to rest of the buffer. - $data .= $buffer; + // Append current buffer to parts array. + $dataParts[] = $buffer; - // Check if we have the ending (.\r\n) - if (str_ends_with($buffer, ".\r\n")) { + // Check if we have the ending (.\r\n) - check last 3 chars directly + $bufLen = \strlen($buffer); + if ($bufLen >= 3 && $buffer[$bufLen - 3] === '.' && $buffer[$bufLen - 2] === "\r" && $buffer[$bufLen - 1] === "\n") { // We have a possible ending, next loop check if it is. $possibleTerm = true; } @@ -1028,32 +1067,35 @@ class NNTP extends \Net_NNTP_Client return $response; } - $body = ''; if ($response === NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS) { + // Use array to accumulate lines (faster than string concatenation for many appends) + $bodyParts = []; + $socket = $this->_socket; + // Continue until connection is lost - while (! feof($this->_socket)) { - // Retrieve and append up to 1024 characters from the server. - $line = fgets($this->_socket, 1024); + while (! feof($socket)) { + // Retrieve and append up to 8192 characters from the server (larger buffer = fewer syscalls) + $line = fgets($socket, 8192); // If the socket is empty/ an error occurs, false is returned. - // Since the socket is blocking, the socket should not be empty, so it's definitely an error. if ($line === false) { return $this->throwError('Failed to read line from socket.', null); } // Check if the line terminates the text response. if ($line === ".\r\n") { - // Attempt to yEnc decode and return the body. + // Join all parts and attempt to yEnc decode + $body = implode('', $bodyParts); return PhpYenc::decodeIgnore($body); } // Check for line that starts with double period, remove one. - if (str_starts_with($line, '.') && $line[1] === '.') { + if ($line[0] === '.' && isset($line[1]) && $line[1] === '.') { $line = substr($line, 1); } - // Add the line to the rest of the lines. - $body .= $line; + // Add the line to the array + $bodyParts[] = $line; } return $this->throwError('End of stream! Connection lost?', null); @@ -1079,13 +1121,13 @@ class NNTP extends \Net_NNTP_Client $retVal = true; } else { switch ($this->_currentServer) { - case config('nntmux_nntp.server'): + case $this->_configServer: if (\is_resource($this->_socket)) { $this->doQuit(true); } $retVal = $this->doConnect(); break; - case config('nntmux_nntp.alternate_server'): + case $this->_configAlternateServer: if (\is_resource($this->_socket)) { $this->doQuit(true); }