diff --git a/packages/web/src/Net/FOGSSH.php b/packages/web/src/Net/FOGSSH.php index 3e4c738769..85453334b5 100644 --- a/packages/web/src/Net/FOGSSH.php +++ b/packages/web/src/Net/FOGSSH.php @@ -152,14 +152,21 @@ public function sftp() */ public function disconnect() { + // Null, never unset(). An unset declared property routes the next + // read of it to __get(), which looks it up in $data and warns + // 'Undefined array key "_link"' on every reuse of this shared + // instance -- the next connect() or disconnect() reads both. if ($this->_sftp) { @ssh2_disconnect($this->_sftp); $this->_sftp = null; - unset($this->_sftp); } - $test = @ssh2_disconnect($this->_link); + // No session to close after a failed connect (false) or an earlier + // disconnect (null). ssh2_disconnect() takes only a resource, and on + // PHP 8 anything else is a TypeError that @ does not suppress. + $test = is_resource($this->_link) + ? @ssh2_disconnect($this->_link) + : false; $this->_link = null; - unset($this->_link); return $test; } /** diff --git a/tests/fogssh-reuse-after-disconnect.test.php b/tests/fogssh-reuse-after-disconnect.test.php new file mode 100644 index 0000000000..4af8f3ba09 --- /dev/null +++ b/tests/fogssh-reuse-after-disconnect.test.php @@ -0,0 +1,56 @@ +disconnect(); + $ssh->disconnect(); +} catch (\Throwable $e) { + $fatal = get_class($e) . ': ' . $e->getMessage(); +} + +restore_error_handler(); + +$t->check( + 'disconnect() without a live session does not throw' + . ($fatal ? " ($fatal)" : ''), + '' === $fatal +); +$t->check( + 'a second disconnect() reads $_link and $_sftp without a warning' + . ($warnings ? ' (' . implode('; ', $warnings) . ')' : ''), + [] === $warnings +); + +$t->finish();