From c7f5f8fdbd9d9e5c9efd9df2688adde23b26e975 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 20 Jul 2026 18:57:45 +0200 Subject: [PATCH 01/24] add: TCP transport class to handle filetransfer connections --- src/Transport/TCP.php | 93 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/Transport/TCP.php diff --git a/src/Transport/TCP.php b/src/Transport/TCP.php new file mode 100644 index 00000000..fe23fcf1 --- /dev/null +++ b/src/Transport/TCP.php @@ -0,0 +1,93 @@ +stream !== null) { + return; + } + + $host = strval($this->config['host']); + $port = strval($this->config['port']); + + $address = 'tcp://'.(str_contains($host, ':') ? '['.$host.']' : $host).':'.$port; + $timeout = (int) $this->config['timeout']; + + $this->stream = @stream_socket_client($address, $errno, $errstr, $timeout); + + if ($this->stream === false) { + $this->stream = null; + + throw new TransportException(StringHelper::factory($errstr)->toUtf8()->toString(), $errno); + } + + stream_set_timeout($this->stream, $timeout); + stream_set_blocking($this->stream, $this->config['blocking'] ? 1 : 0); + } + + /** + * Disconnects from a remote server. + */ + public function disconnect(): void + { + if ($this->stream === null) { + return; + } + + fclose($this->stream); + $this->stream = null; + + Signal::getInstance()->emit(strtolower($this->getAdapterType()).'Disconnected'); + } + + /** + * Reads data from the stream. + * + * @throws TransportException + */ + public function read(int $length = 4096): StringHelper + { + $this->connect(); + $this->waitForReadyRead(); + + $data = fread($this->stream, $length); + + if ($data === false) { + throw new TransportException("connection to server '".$this->config['host'].':'.$this->config['port']."' lost"); + } + + Signal::getInstance()->emit(strtolower($this->getAdapterType()).'DataRead', $data); + + return new StringHelper($data); + } + + /** + * Writes data to the stream. + * + * @throws TransportException + */ + public function send(string $data): void + { + $this->connect(); + + $written = fwrite($this->stream, $data); + + if ($written === false) { + throw new TransportException("connection to server '".$this->config['host'].':'.$this->config['port']."' lost"); + } + + Signal::getInstance()->emit(strtolower($this->getAdapterType()).'DataSend', $data); + } +} From d2c825f4ae457690e87df9763d0b94b2028ea436 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 20 Jul 2026 18:58:36 +0200 Subject: [PATCH 02/24] update: refactor file transfer methods --- src/Adapter/FileTransfer.php | 57 ++++++----- src/Node/Channel.php | 38 ++++++++ src/Node/Server.php | 181 ++++++++++++++++++++++++++++++++--- 3 files changed, 241 insertions(+), 35 deletions(-) diff --git a/src/Adapter/FileTransfer.php b/src/Adapter/FileTransfer.php index 89941041..97f9aa69 100644 --- a/src/Adapter/FileTransfer.php +++ b/src/Adapter/FileTransfer.php @@ -7,6 +7,7 @@ use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Profiler; use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Signal; use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper; +use PlanetTeamSpeak\TeamSpeak3Framework\Transport\TCP; use PlanetTeamSpeak\TeamSpeak3Framework\Transport\Transport; /** @@ -21,7 +22,7 @@ class FileTransfer extends Adapter */ public function syn(): void { - $this->initTransport($this->options); + $this->initTransport($this->options, TCP::class); $this->transport->setAdapter($this); Profiler::init(spl_object_hash($this)); @@ -42,14 +43,14 @@ public function __destruct() /** * Sends a valid file transfer key to the server to initialize the file transfer. * - * @param string $ftkey + * @param string $ftkey * @return void * @throws FileTransferException * @throws TransportException */ protected function init(string $ftkey): void { - if (strlen($ftkey) != 32 && strlen($ftkey) != 16) { + if (strlen($ftkey) !== 32 && strlen($ftkey) !== 16) { throw new FileTransferException('invalid file transfer key format'); } @@ -62,9 +63,9 @@ protected function init(string $ftkey): void /** * Sends the content of a file to the server. * - * @param string $ftkey - * @param int $seek - * @param string $data + * @param string $ftkey + * @param int $seek + * @param string $data * @return void * @throws FileTransferException * @throws TransportException @@ -78,14 +79,15 @@ public function upload(string $ftkey, int $seek, string $data): void Signal::getInstance()->emit('filetransferUploadStarted', $ftkey, $seek, $size); - for (; $seek < $size;) { + while ($seek < $size) { $rest = $size - $seek; - $pack = min($rest, $pack); - $buff = substr($data, $seek, $pack); - $seek = $seek + $pack; + $chunkSize = min($rest, $pack); + $buff = substr($data, $seek, $chunkSize); $this->getTransport()->send($buff); + $seek += $chunkSize; + Signal::getInstance()->emit('filetransferUploadProgress', $ftkey, $seek, $size); } @@ -97,9 +99,9 @@ public function upload(string $ftkey, int $seek, string $data): void /** * Returns the content of a downloaded file as a PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper object. * - * @param string $ftkey - * @param int $size - * @param bool $passthru + * @param string $ftkey + * @param int $size + * @param bool $passthru * @return StringHelper|void * @throws FileTransferException * @throws TransportException @@ -119,13 +121,16 @@ public function download(string $ftkey, int $size, bool $passthru = false) Signal::getInstance()->emit('filetransferDownloadStarted', $ftkey, count($buff), $size); - for ($seek = 0; $seek < $size;) { - $rest = $size - $seek; - $pack = min($rest, $pack); - $data = $this->getTransport()->read(min($rest, $pack)); - $seek = $seek + $pack; + while (count($buff) < $size) { + $rest = $size - count($buff); + $chunkSize = min($rest, $pack); + $data = $this->getTransport()->read($chunkSize); + + if (count($data) === 0) { + break; + } - $buff->append($data); + $buff->append($data->toString()); Signal::getInstance()->emit('filetransferDownloadProgress', $ftkey, count($buff), $size); } @@ -134,7 +139,7 @@ public function download(string $ftkey, int $size, bool $passthru = false) Signal::getInstance()->emit('filetransferDownloadFinished', $ftkey, count($buff), $size); - if (strlen($buff) != $size) { + if (count($buff) !== $size) { throw new FileTransferException('incomplete file download ('.count($buff).' of '.$size.' bytes)'); } @@ -151,10 +156,16 @@ public function download(string $ftkey, int $size, bool $passthru = false) */ protected function passthru(int $size): void { - $buff_size = fpassthru($this->getTransport()->getStream()); + $stream = $this->getTransport()->getStream(); + + if (! is_resource($stream)) { + throw new FileTransferException('invalid file transfer stream'); + } + + $buffSize = fpassthru($stream); - if ($buff_size != $size) { - throw new FileTransferException('incomplete file download ('.$buff_size.' of '.$size.' bytes)'); + if ($buffSize !== $size) { + throw new FileTransferException('incomplete file download ('.$buffSize.' of '.$size.' bytes)'); } } } diff --git a/src/Node/Channel.php b/src/Node/Channel.php index 179a67c3..79e2595c 100644 --- a/src/Node/Channel.php +++ b/src/Node/Channel.php @@ -332,6 +332,44 @@ public function dirCreate(string $cpw = '', string $dirname = '/'): void $this->getParent()->channelDirCreate($this->getId(), $cpw, $dirname); } + /** + * Downloads a file from this channel file repository. + * + * @param string $name + * @param string $cpw + * @param int $seekpos + * @return StringHelper + * @throws AdapterException + * @throws FileTransferException + * @throws ServerQueryException + * @throws TransportException + * @throws \Exception + */ + public function fileDownload(string $name, string $cpw = '', int $seekpos = 0): StringHelper + { + return $this->getParent()->channelFileDownload($this->getId(), $name, $cpw, $seekpos); + } + + /** + * Uploads a file into this channel file repository. + * + * @param string $name + * @param string $data + * @param string $cpw + * @param bool $overwrite + * @param bool $resume + * @return void + * @throws AdapterException + * @throws FileTransferException + * @throws ServerQueryException + * @throws TransportException + * @throws \Exception + */ + public function fileUpload(string $name, string $data, string $cpw = '', bool $overwrite = false, bool $resume = false) :void + { + $this->getParent()->channelFileUpload($this->getId(), $name, $data, $cpw, $overwrite, $resume); + } + /** * Returns the level of the channel. * diff --git a/src/Node/Server.php b/src/Node/Server.php index 6cb53417..bc29c12a 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2077,6 +2077,47 @@ public function permRemoveAny(int $permid): int return count($assignments); } + /** + * Removes protocol/meta rows from a file transfer init response and returns the row containing ftkey/port. + * + * @param array $response + * @return array + * @throws ServerQueryException + */ + protected function normalizeTransferInitResponse(array $response): array + { + if (isset($response['ftkey']) || isset($response['port'])) { + return $response; + } + + foreach ($response as $row) { + if (! is_array($row)) { + continue; + } + + if (isset($row['ftkey']) || isset($row['port'])) { + return $row; + } + } + + throw new ServerQueryException('invalid file transfer init response'); + } + + /** + * Initializes a file transfer upload. $clientftfid is an arbitrary ID to identify the file transfer on the client-side. + * + * @param int $clientftfid + * @param int $cid + * @param string $name + * @param int $size + * @param string $cpw + * @param bool $overwrite + * @param bool $resume + * @return array + * @throws AdapterException + * @throws ServerQueryException + * @throws TransportException + */ /** * Initializes a file transfer upload. $clientftfid is an arbitrary ID to identify the file transfer on the client-side. * @@ -2101,21 +2142,31 @@ public function transferInitUpload( bool $overwrite = false, bool $resume = false ): array { - $upload = $this->execute('ftinitupload', ['clientftfid' => $clientftfid, 'cid' => $cid, 'name' => $name, 'cpw' => $cpw, 'size' => $size, 'overwrite' => $overwrite, 'resume' => $resume]) - ->toList(); + $response = $this->execute('ftinitupload', [ + 'clientftfid' => $clientftfid, + 'cid' => $cid, + 'name' => $name, + 'cpw' => $cpw, + 'size' => $size, + 'overwrite' => $overwrite, + 'resume' => $resume, + ])->toList(); + + $upload = $this->normalizeTransferInitResponse($response); if (array_key_exists('status', $upload) && $upload['status'] != 0x00) { - throw new ServerQueryException($upload['msg'], $upload['status']); + throw new ServerQueryException((string) $upload['msg'], (int) $upload['status']); } $upload['cid'] = $cid; $upload['file'] = $name; - if (! array_key_exists('ip', $upload) || $upload['ip']->startsWith('0.0.0.0')) { + if (! array_key_exists('ip', $upload) || (string) $upload['ip'] === '' || (string) $upload['ip'] === '0.0.0.0') { $upload['ip'] = $this->getParent()->getAdapterHost(); - } else { + } elseif ($upload['ip'] instanceof StringHelper) { $upload['ip'] = $upload['ip']->section(','); } + $upload['host'] = $upload['ip']; Signal::getInstance()->emit('filetransferUploadInit', $upload['ftkey'], $upload); @@ -2138,21 +2189,29 @@ public function transferInitUpload( */ public function transferInitDownload(int $clientftfid, int $cid, string $name, string $cpw = '', int $seekpos = 0): array { - $download = $this->execute('ftinitdownload', ['clientftfid' => $clientftfid, 'cid' => $cid, 'name' => $name, 'cpw' => $cpw, 'seekpos' => $seekpos]) - ->toList(); + $response = $this->execute('ftinitdownload', [ + 'clientftfid' => $clientftfid, + 'cid' => $cid, + 'name' => $name, + 'cpw' => $cpw, + 'seekpos' => $seekpos, + ])->toList(); + + $download = $this->normalizeTransferInitResponse($response); if (array_key_exists('status', $download) && $download['status'] != 0x00) { - throw new ServerQueryException($download['msg'], $download['status']); + throw new ServerQueryException((string) $download['msg'], (int) $download['status']); } $download['cid'] = $cid; $download['file'] = $name; - if (! array_key_exists('ip', $download) || $download['ip']->startsWith('0.0.0.0')) { + if (! array_key_exists('ip', $download) || (string) $download['ip'] === '' || (string) $download['ip'] === '0.0.0.0') { $download['ip'] = $this->getParent()->getAdapterHost(); - } else { + } elseif ($download['ip'] instanceof StringHelper) { $download['ip'] = $download['ip']->section(','); } + $download['host'] = $download['ip']; Signal::getInstance()->emit('filetransferDownloadInit', $download['ftkey'], $download); @@ -2189,6 +2248,104 @@ public function transferStop(int $serverftfid, bool $delete = false): void $this->execute('ftstop', ['serverftfid' => $serverftfid, 'delete' => $delete]); } + /** + * Builds a valid filetransfer URI from ftinitupload/ftinitdownload response data. + * + * @param array $transfer + * @return string + * @throws ServerQueryException + */ + protected function buildFileTransferUri(array $transfer): string + { + $host = $transfer['host'] ?? $transfer['ip'] ?? $this->getParent()->getAdapterHost(); + $port = $transfer['port'] ?? null; + + if ($host instanceof StringHelper) { + $host = $host->toString(); + } + + if ($port instanceof StringHelper) { + $port = $port->toString(); + } + + $host = trim((string) $host); + $port = is_numeric($port) ? (int) $port : 0; + + if ($host === '' || $host === '0.0.0.0') { + $host = $this->getParent()->getAdapterHost(); + } + + if ($host instanceof StringHelper) { + $host = $host->toString(); + } + + $host = trim((string) $host); + + if ($host === '') { + throw new ServerQueryException('invalid file transfer host'); + } + + if ($port < 1 || $port > 65535) { + throw new ServerQueryException('invalid file transfer port: '.json_encode($transfer)); + } + + return 'filetransfer://'.(str_contains($host, ':') ? '['.$host.']' : $host).':'.$port; + } + + /** + * Downloads a file from a channel file repository. + * + * @param int $cid + * @param string $name + * @param string $cpw + * @param int $seekpos + * @return StringHelper + * @throws AdapterException + * @throws FileTransferException + * @throws ServerQueryException + * @throws TransportException + * @throws Exception + */ + public function channelFileDownload(int $cid, string $name, string $cpw = '', int $seekpos = 0): StringHelper + { + $download = $this->transferInitDownload(rand(0x0000, 0xFFFF), $cid, $name, $cpw, $seekpos); + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($download)); + + return $transfer->download($download['ftkey'], $download['size']); + } + + /** + * Uploads a file into a channel file repository. + * + * @param int $cid + * @param string $name + * @param string $data + * @param string $cpw + * @param bool $overwrite + * @param bool $resume + * @return void + * @throws AdapterException + * @throws FileTransferException + * @throws ServerQueryException + * @throws TransportException + * @throws Exception + */ + public function channelFileUpload(int $cid, string $name, string $data, string $cpw = '', bool $overwrite = false, bool $resume = false): void + { + $upload = $this->transferInitUpload( + rand(0x0000, 0xFFFF), + $cid, + $name, + strlen($data), + $cpw, + $overwrite, + $resume + ); + + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($upload)); + $transfer->upload($upload['ftkey'], $upload['seekpos'], $data); + } + /** * Downloads and returns the servers icon file content. * @@ -2218,7 +2375,7 @@ public function iconDownload(string $iconname = null) } $download = $this->transferInitDownload(rand(0x0000, 0xFFFF), 0, $name); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port']); + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($download)); return $transfer->download($download['ftkey'], $download['size']); } @@ -2240,7 +2397,7 @@ public function iconUpload(string $data): int $size = strlen($data); $upload = $this->transferInitUpload(rand(0x0000, 0xFFFF), 0, '/icon_'.$crc, $size); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($upload['host'], ':') ? '['.$upload['host'].']' : $upload['host']).':'.$upload['port']); + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($upload)); $transfer->upload($upload['ftkey'], $upload['seekpos'], $data); From 77e5940a02f4f26af220a4d148ff7fee596bdaa5 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 20 Jul 2026 18:58:48 +0200 Subject: [PATCH 03/24] add: FileTransferTest class for testing --- tests/DevLiveServer/FIleTransferTest.php | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/DevLiveServer/FIleTransferTest.php diff --git a/tests/DevLiveServer/FIleTransferTest.php b/tests/DevLiveServer/FIleTransferTest.php new file mode 100644 index 00000000..58f45691 --- /dev/null +++ b/tests/DevLiveServer/FIleTransferTest.php @@ -0,0 +1,77 @@ +active = str_replace('DEV_LIVE_SERVER_AVAILABLE=', '', preg_replace('#\n(?!\n)#', '', $env[2])); + $this->host = str_replace('DEV_LIVE_SERVER_HOST=', '', preg_replace('#\n(?!\n)#', '', $env[3])); + $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); + $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); + $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + } else { + $this->active = 'false'; + } + + $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. + '/?server_port=9987'. + '&no_query_clients=0'. + '&blocking=0'. + '&timeout=30'; + } + + /** + * @throws AdapterException + * @throws ServerQueryException + * @throws TransportException + * @throws \Exception + */ + public function test_can_upload_download(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + + $channel = $ts3_Host->channelGetByName('UnitTest'); + $channel->fileUpload('/test.txt', 'Hallo Welt', overwrite: true); + $content = $channel->fileDownload('/test.txt'); + + $this->assertEquals('Hallo Welt', $content->toString()); + + $channel->fileDelete('', '/test.txt'); + $ts3_Host->getAdapter()->getTransport()->disconnect(); + } + +} From f3ac7de441929c7828c062c6918745f2ad14dcd7 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 21 Jul 2026 21:31:53 +0200 Subject: [PATCH 04/24] fix: replace static server port into dynamic variable in test classes --- .env.testing.example | 4 +++- tests/DevLiveServer/ChannelGroupTest.php | 5 ++++- tests/DevLiveServer/ChannelTest.php | 5 ++++- tests/DevLiveServer/ClientTest.php | 5 ++++- tests/DevLiveServer/ConnectionTest.php | 21 ++++++++++++------- tests/DevLiveServer/FIleTransferTest.php | 9 +++++--- .../FoundIssuesOnPublicProjectsTest.php | 5 ++++- tests/DevLiveServer/RefactorFunctionsTest.php | 5 ++++- tests/DevLiveServer/ServerGroupTest.php | 5 ++++- tests/DevLiveServer/SignalTest.php | 5 ++++- 10 files changed, 50 insertions(+), 19 deletions(-) diff --git a/.env.testing.example b/.env.testing.example index 6f91497b..62c677a5 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -9,4 +9,6 @@ DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=UnitTest DEV_LIVE_SERVER_UNIT_TEST_USER_ACTIVE=false DEV_LIVE_SERVER_UNIT_TEST_USER=UnitTestUser DEV_LIVE_SERVER_UNIT_TEST_SIGNALS=false -DEV_LIVE_SERVER_UNIT_TEST_USER_EXTEND= +DEV_LIVE_SERVER_UNIT_TEST_USER_EXTEND=UnitTestUser2 +DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=9987 +DEV_LIVE_SERVER_UNIT_TEST_SERVER_QUERY_LOGIN_NAME=ts3-bot-dev diff --git a/tests/DevLiveServer/ChannelGroupTest.php b/tests/DevLiveServer/ChannelGroupTest.php index c9e1ca02..1567b12a 100644 --- a/tests/DevLiveServer/ChannelGroupTest.php +++ b/tests/DevLiveServer/ChannelGroupTest.php @@ -27,6 +27,8 @@ class ChannelGroupTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $user_test_active; @@ -57,12 +59,13 @@ public function setUp(): void $this->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7])); $this->user_test_active = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_ACTIVE=', '', preg_replace('#\n(?!\n)#', '', $env[8])); $this->ts3_unit_test_userName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER=', '', preg_replace('#\n(?!\n)#', '', $env[9])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&timeout=30'; } diff --git a/tests/DevLiveServer/ChannelTest.php b/tests/DevLiveServer/ChannelTest.php index cbf5e0a2..4b6e9eca 100644 --- a/tests/DevLiveServer/ChannelTest.php +++ b/tests/DevLiveServer/ChannelTest.php @@ -25,6 +25,8 @@ class ChannelTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -47,12 +49,13 @@ public function setUp(): void $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); $this->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&timeout=30'; } diff --git a/tests/DevLiveServer/ClientTest.php b/tests/DevLiveServer/ClientTest.php index c11aa20c..fdb9518d 100644 --- a/tests/DevLiveServer/ClientTest.php +++ b/tests/DevLiveServer/ClientTest.php @@ -25,6 +25,8 @@ class ClientTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -58,12 +60,13 @@ public function setUp(): void $this->user_test_active = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_ACTIVE=', '', preg_replace('#\n(?!\n)#', '', $env[8])); $this->ts3_unit_test_userName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER=', '', preg_replace('#\n(?!\n)#', '', $env[9])); $this->ts3_unit_test_userName2 = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_EXTEND=', '', preg_replace('#\n(?!\n)#', '', $env[11])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&blocking=0'. '&timeout=30'; diff --git a/tests/DevLiveServer/ConnectionTest.php b/tests/DevLiveServer/ConnectionTest.php index 3cae0eb3..fc0d99a4 100644 --- a/tests/DevLiveServer/ConnectionTest.php +++ b/tests/DevLiveServer/ConnectionTest.php @@ -23,29 +23,34 @@ class ConnectionTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; private string $ts3_server_uri; + private string $serverQueryLoginName; + public function setUp(): void { //proof test active if (file_exists('./.env.testing')) { $env = file('./.env.testing'); - //get live server is available $this->active = str_replace('DEV_LIVE_SERVER_AVAILABLE=', '', preg_replace('#\n(?!\n)#', '', $env[2])); $this->host = str_replace('DEV_LIVE_SERVER_HOST=', '', preg_replace('#\n(?!\n)#', '', $env[3])); $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); + $this->serverQueryLoginName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_QUERY_LOGIN_NAME=', '', preg_replace('#\n(?!\n)#', '', $env[13])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&blocking=0'. '&timeout=30'; @@ -145,7 +150,7 @@ public function test_can_get_host_information() $ts3_host = TeamSpeak3::factory($this->ts3_server_uri); $port = $ts3_host->getParent()->serverSelectedPort(); - $this->assertEquals(9987, $port); + $this->assertEquals($this->serverPort, $port); $version = $ts3_host->version(); $this->assertIsArray($version); @@ -154,13 +159,13 @@ public function test_can_get_host_information() $this->assertEquals('Linux', $version['platform']); $this->assertArrayHasKey('build', $version); - $serverID = $ts3_host->serverIdGetByPort(9987); + $serverID = $ts3_host->serverIdGetByPort($this->serverPort); $this->assertIsInt($serverID); $this->assertEquals(1, $serverID); $PortByID = $ts3_host->serverGetPortById($serverID); $this->assertIsInt($PortByID); - $this->assertEquals(9987, $PortByID); + $this->assertEquals($this->serverPort, $PortByID); $server = $ts3_host->servergetByname('UnitTestServer'); $this->assertIsArray($server); @@ -240,8 +245,8 @@ public function test_can_handle_log() } $ts3_host = TeamSpeak3::factory($this->ts3_server_uri); - $ts3_host->serverGetByPort(9987)->logAdd('UnitTest', TeamSpeak3::LOGLEVEL_DEBUG); - $log = $ts3_host->serverGetByPort(9987)->logView(); + $ts3_host->serverGetByPort($this->serverPort)->logAdd('UnitTest', TeamSpeak3::LOGLEVEL_DEBUG); + $log = $ts3_host->serverGetByPort($this->serverPort)->logView(); $this->assertIsArray($log); $this->assertIsString($log[29]); $this->assertStringContainsString('UnitTest', $log[29]); @@ -270,7 +275,7 @@ public function test_can_handle_server_query() foreach ($queryLoginlist as $queryLogin) { $this->assertIsString($queryLogin['client_login_name']); - $this->assertEquals('ts3-bot-dev', $queryLogin['client_login_name']); + $this->assertEquals($this->serverQueryLoginName, $queryLogin['client_login_name']); } $ts3_host->getAdapter()->getTransport()->disconnect(); diff --git a/tests/DevLiveServer/FIleTransferTest.php b/tests/DevLiveServer/FIleTransferTest.php index 58f45691..1e1a8d27 100644 --- a/tests/DevLiveServer/FIleTransferTest.php +++ b/tests/DevLiveServer/FIleTransferTest.php @@ -22,6 +22,8 @@ class FIleTransferTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -39,12 +41,13 @@ public function setUp(): void $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&blocking=0'. '&timeout=30'; @@ -65,10 +68,10 @@ public function test_can_upload_download(): void $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); $channel = $ts3_Host->channelGetByName('UnitTest'); - $channel->fileUpload('/test.txt', 'Hallo Welt', overwrite: true); + $channel->fileUpload('/test.txt', 'Hello World', overwrite: true); $content = $channel->fileDownload('/test.txt'); - $this->assertEquals('Hallo Welt', $content->toString()); + $this->assertEquals('Hello World', $content->toString()); $channel->fileDelete('', '/test.txt'); $ts3_Host->getAdapter()->getTransport()->disconnect(); diff --git a/tests/DevLiveServer/FoundIssuesOnPublicProjectsTest.php b/tests/DevLiveServer/FoundIssuesOnPublicProjectsTest.php index 3bacda92..f537a4b7 100644 --- a/tests/DevLiveServer/FoundIssuesOnPublicProjectsTest.php +++ b/tests/DevLiveServer/FoundIssuesOnPublicProjectsTest.php @@ -24,6 +24,8 @@ class FoundIssuesOnPublicProjectsTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -43,12 +45,13 @@ public function setUp(): void $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&timeout=30'; } diff --git a/tests/DevLiveServer/RefactorFunctionsTest.php b/tests/DevLiveServer/RefactorFunctionsTest.php index 123a8e63..d540cb12 100644 --- a/tests/DevLiveServer/RefactorFunctionsTest.php +++ b/tests/DevLiveServer/RefactorFunctionsTest.php @@ -27,6 +27,8 @@ class RefactorFunctionsTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -46,12 +48,13 @@ public function setUp(): void $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&timeout=30'; } diff --git a/tests/DevLiveServer/ServerGroupTest.php b/tests/DevLiveServer/ServerGroupTest.php index e0a01cd1..d40b3396 100644 --- a/tests/DevLiveServer/ServerGroupTest.php +++ b/tests/DevLiveServer/ServerGroupTest.php @@ -28,6 +28,8 @@ class ServerGroupTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -49,12 +51,13 @@ public function setUp(): void $this->queryPort = str_replace('DEV_LIVE_SERVER_QUERY_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[4])); $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&timeout=30'; } diff --git a/tests/DevLiveServer/SignalTest.php b/tests/DevLiveServer/SignalTest.php index 47960741..53eb3189 100644 --- a/tests/DevLiveServer/SignalTest.php +++ b/tests/DevLiveServer/SignalTest.php @@ -31,6 +31,8 @@ class SignalTest extends TestCase private string $queryPort; + private string $serverPort; + private string $user; private string $password; @@ -55,12 +57,13 @@ public function setUp(): void $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); $this->ts3_unit_test_signals = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SIGNALS=', '', preg_replace('#\n(?!\n)#', '', $env[10])); + $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); } else { $this->active = 'false'; } $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort. - '/?server_port=9987'. + '/?server_port='.$this->serverPort. '&no_query_clients=0'. '&blocking=0'. '&timeout=30'; From b7e6693b62b3617026aa6e11a4d3fbd2b8a13da9 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 21 Jul 2026 22:04:18 +0200 Subject: [PATCH 05/24] fix: typo in FileTransferTest class name --- .../{FIleTransferTest.php => FileTransferTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/DevLiveServer/{FIleTransferTest.php => FileTransferTest.php} (98%) diff --git a/tests/DevLiveServer/FIleTransferTest.php b/tests/DevLiveServer/FileTransferTest.php similarity index 98% rename from tests/DevLiveServer/FIleTransferTest.php rename to tests/DevLiveServer/FileTransferTest.php index 1e1a8d27..72a6f290 100644 --- a/tests/DevLiveServer/FIleTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -8,7 +8,7 @@ use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException; use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3; -class FIleTransferTest extends TestCase +class FileTransferTest extends TestCase { /** * ATTENTION From aee1576e866798e9516b94536124a7dca63bf767 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 21 Jul 2026 22:05:18 +0200 Subject: [PATCH 06/24] fix: unescaped ftkey in file transfer with ts3 server --- src/Node/Server.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Node/Server.php b/src/Node/Server.php index bc29c12a..9f2b16e4 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2309,9 +2309,10 @@ protected function buildFileTransferUri(array $transfer): string public function channelFileDownload(int $cid, string $name, string $cpw = '', int $seekpos = 0): StringHelper { $download = $this->transferInitDownload(rand(0x0000, 0xFFFF), $cid, $name, $cpw, $seekpos); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); $transfer = TeamSpeak3::factory($this->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** @@ -2342,8 +2343,10 @@ public function channelFileUpload(int $cid, string $name, string $data, string $ $resume ); + $ftkey = str_replace('\\/', '/', (string) $upload['ftkey']); + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($upload)); - $transfer->upload($upload['ftkey'], $upload['seekpos'], $data); + $transfer->upload($ftkey, (int) $upload['seekpos'], $data); } /** From 00df08945549f13a143e200135f99528dd86e5d8 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Wed, 22 Jul 2026 18:33:08 +0200 Subject: [PATCH 07/24] change: separate filetransfer tests into upload, download and delete add: testfolder to handle testdata, icons etc --- tests/DevLiveServer/FileTransferTest.php | 60 +++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index 72a6f290..03ef99dd 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -4,6 +4,8 @@ use PHPUnit\Framework\TestCase; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException; +use PlanetTeamSpeak\TeamSpeak3Framework\Exception\FileTransferException; +use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException; use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3; @@ -30,6 +32,8 @@ class FileTransferTest extends TestCase private string $ts3_server_uri; + private string $testPath = DIRECTORY_SEPARATOR . 'tests\testsources'; + public function setUp(): void { //proof test active @@ -59,7 +63,7 @@ public function setUp(): void * @throws TransportException * @throws \Exception */ - public function test_can_upload_download(): void + public function test_can_file_upload(): void { if ($this->active == 'false') { $this->markTestSkipped('DevLiveServer ist not active'); @@ -69,11 +73,63 @@ public function test_can_upload_download(): void $channel = $ts3_Host->channelGetByName('UnitTest'); $channel->fileUpload('/test.txt', 'Hello World', overwrite: true); + $cFileList = $channel->fileList(); + + $this->assertCount(1, $cFileList); + $this->assertEquals('test.txt', $cFileList[0]['name']); + + $ts3_Host->getAdapter()->getTransport()->disconnect(); + } + + /** + * @throws FileTransferException + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws HelperException + */ + public function test_can_file_download(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + + $channel = $ts3_Host->channelGetByName('UnitTest'); $content = $channel->fileDownload('/test.txt'); + file_put_contents(getcwd() . $this->testPath .'\test.txt', $content->toString()); + $this->assertFileExists(getcwd() . $this->testPath .'\test.txt'); $this->assertEquals('Hello World', $content->toString()); - $channel->fileDelete('', '/test.txt'); + $ts3_Host->getAdapter()->getTransport()->disconnect(); + } + + /** + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws HelperException + */ + public function test_can_file_delete(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + $channel = $ts3_Host->channelGetByName('UnitTest'); + $cFileList = $channel->fileList(); + + if(count($cFileList) > 0) + { + $channel->fileDelete('', '/test.txt'); + } + + $cFileList = $channel->fileList(); + $this->assertCount(0, $cFileList); + $ts3_Host->getAdapter()->getTransport()->disconnect(); } From 848868fea13c0dcdd3dd708345811bec7f983931 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 16:31:06 +0200 Subject: [PATCH 08/24] add: proof is icon file is readable, replace ftkey unescape chars --- src/Node/Server.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Node/Server.php b/src/Node/Server.php index 9f2b16e4..a282812d 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2387,22 +2387,33 @@ public function iconDownload(string $iconname = null) * Uploads a given icon file content to the server and returns the ID of the icon. * * @param string $data + * @param bool $overwrite * @return int * @throws AdapterException * @throws FileTransferException + * @throws HelperException * @throws ServerQueryException * @throws TransportException - * @throws Exception */ - public function iconUpload(string $data): int + public function iconUpload(string $data, bool $overwrite = true): int { + + if (is_file($data)) { + $data = file_get_contents($data); + + if ($data === false) { + throw new ServerQueryException('unable to read icon file'); + } + } + $crc = crc32($data); $size = strlen($data); - $upload = $this->transferInitUpload(rand(0x0000, 0xFFFF), 0, '/icon_'.$crc, $size); - $transfer = TeamSpeak3::factory($this->buildFileTransferUri($upload)); + $upload = $this->transferInitUpload(rand(0x0000, 0xFFFF), 0, '/icon_'.$crc, $size, '', $overwrite); + $ftkey = str_replace('\\/', '/', (string) $upload['ftkey']); - $transfer->upload($upload['ftkey'], $upload['seekpos'], $data); + $transfer = TeamSpeak3::factory($this->buildFileTransferUri($upload)); + $transfer->upload($ftkey, (int) $upload['seekpos'], $data); return $crc; } From 5d1c133056b704b436f38ac46c35497a787e4622 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 17:22:10 +0200 Subject: [PATCH 09/24] add: function delete icon server side --- src/Node/Server.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Node/Server.php b/src/Node/Server.php index a282812d..40faaa29 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2418,6 +2418,22 @@ public function iconUpload(string $data, bool $overwrite = true): int return $crc; } + /** + * @throws AdapterException + * @throws ServerQueryException + * @throws TransportException + */ + public function iconDelete(string|int $icon): void + { + $name = (string) $icon; + + if (! str_starts_with($name, 'icon_')) { + $name = 'icon_'.$name; + } + + $this->channelFileDelete(0, '', '/'.$name); + } + /** * Changes the virtual server configuration using given properties. * From f734ba3da1aaef4e744d9f91751d123d033eec2f Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 17:22:46 +0200 Subject: [PATCH 10/24] add: test upload, download and delete icon --- tests/DevLiveServer/FileTransferTest.php | 88 ++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index 03ef99dd..ac140751 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -133,4 +133,92 @@ public function test_can_file_delete(): void $ts3_Host->getAdapter()->getTransport()->disconnect(); } + /** + * @throws AdapterException + * @throws FileTransferException + * @throws TransportException + * @throws ServerQueryException + * @throws HelperException + */ + public function test_can_icon_upload(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + $iconId = $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + $iconList = $ts3_Host->iconList(); + + $this->assertNotEmpty($iconList); + $this->assertContains('icon_'.$iconId, array_column($iconList, 'name')); + + $ts3_Host->getAdapter()->getTransport()->disconnect(); + + } + + /** + * @throws FileTransferException + * @throws AdapterException + * @throws TransportException + * @throws ServerQueryException + * @throws HelperException + */ + public function test_can_icon_download(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + $iconList = $ts3_Host->iconList(); + + if ($iconList === []) { + $this->markTestIncomplete('No icons available for download'); + } + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + + foreach ($iconList as $icon) { + $this->assertArrayHasKey('name', $icon); + + $content = $ts3_Host->iconDownload($icon['name']); + + $this->assertNotNull($content); + + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$icon['name']; + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + } + + $ts3_Host->getAdapter()->getTransport()->disconnect(); + } + + /** + * @throws AdapterException + * @throws FileTransferException + * @throws TransportException + * @throws ServerQueryException + * @throws HelperException + */ + public function test_can_icon_delete(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); + $iconID = $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + + $ts3_Host->iconDelete('icon_'.$iconID); + + $iconList = $ts3_Host->iconList(); + $this->assertNotContains('icon_'.$iconID, array_column($iconList, 'name')); + + $ts3_Host->getAdapter()->getTransport()->disconnect(); + } + } From 039c0c752ecf9f00f9bef4c0a633b37d5c64bf69 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 17:30:42 +0200 Subject: [PATCH 11/24] chore: code-style --- src/Node/Server.php | 1 - tests/DevLiveServer/FileTransferTest.php | 17 +++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Node/Server.php b/src/Node/Server.php index 40faaa29..fd5c56c5 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2397,7 +2397,6 @@ public function iconDownload(string $iconname = null) */ public function iconUpload(string $data, bool $overwrite = true): int { - if (is_file($data)) { $data = file_get_contents($data); diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index ac140751..84233fcc 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -32,7 +32,7 @@ class FileTransferTest extends TestCase private string $ts3_server_uri; - private string $testPath = DIRECTORY_SEPARATOR . 'tests\testsources'; + private string $testPath = DIRECTORY_SEPARATOR.'tests\testsources'; public function setUp(): void { @@ -98,9 +98,9 @@ public function test_can_file_download(): void $channel = $ts3_Host->channelGetByName('UnitTest'); $content = $channel->fileDownload('/test.txt'); - file_put_contents(getcwd() . $this->testPath .'\test.txt', $content->toString()); + file_put_contents(getcwd().$this->testPath.'\test.txt', $content->toString()); - $this->assertFileExists(getcwd() . $this->testPath .'\test.txt'); + $this->assertFileExists(getcwd().$this->testPath.'\test.txt'); $this->assertEquals('Hello World', $content->toString()); $ts3_Host->getAdapter()->getTransport()->disconnect(); @@ -122,8 +122,7 @@ public function test_can_file_delete(): void $channel = $ts3_Host->channelGetByName('UnitTest'); $cFileList = $channel->fileList(); - if(count($cFileList) > 0) - { + if (count($cFileList) > 0) { $channel->fileDelete('', '/test.txt'); } @@ -147,14 +146,13 @@ public function test_can_icon_upload(): void } $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); - $iconId = $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + $iconId = $ts3_Host->iconUpload(getcwd().$this->testPath.'\icons\upload\ChannelAdmin.png'); $iconList = $ts3_Host->iconList(); $this->assertNotEmpty($iconList); $this->assertContains('icon_'.$iconId, array_column($iconList, 'name')); $ts3_Host->getAdapter()->getTransport()->disconnect(); - } /** @@ -171,7 +169,7 @@ public function test_can_icon_download(): void } $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); - $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + $ts3_Host->iconUpload(getcwd().$this->testPath.'\icons\upload\ChannelAdmin.png'); $iconList = $ts3_Host->iconList(); if ($iconList === []) { @@ -211,7 +209,7 @@ public function test_can_icon_delete(): void } $ts3_Host = TeamSpeak3::factory($this->ts3_server_uri); - $iconID = $ts3_Host->iconUpload(getcwd() . $this->testPath . '\icons\upload\ChannelAdmin.png'); + $iconID = $ts3_Host->iconUpload(getcwd().$this->testPath.'\icons\upload\ChannelAdmin.png'); $ts3_Host->iconDelete('icon_'.$iconID); @@ -220,5 +218,4 @@ public function test_can_icon_delete(): void $ts3_Host->getAdapter()->getTransport()->disconnect(); } - } From 75d6b3a2a1e78a98f55d2e40b8e0aa9c905e1b80 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 17:31:37 +0200 Subject: [PATCH 12/24] add: testdata for icons --- tests/testsources/.gitignore | 4 ++++ tests/testsources/icons/upload/ChannelAdmin.png | Bin 0 -> 3525 bytes tests/testsources/icons/upload/Guest.png | Bin 0 -> 3529 bytes tests/testsources/icons/upload/Registered.png | Bin 0 -> 3431 bytes 4 files changed, 4 insertions(+) create mode 100644 tests/testsources/.gitignore create mode 100644 tests/testsources/icons/upload/ChannelAdmin.png create mode 100644 tests/testsources/icons/upload/Guest.png create mode 100644 tests/testsources/icons/upload/Registered.png diff --git a/tests/testsources/.gitignore b/tests/testsources/.gitignore new file mode 100644 index 00000000..62a18f77 --- /dev/null +++ b/tests/testsources/.gitignore @@ -0,0 +1,4 @@ +* +icons/download* +!.gitignore +!/icons/upload* diff --git a/tests/testsources/icons/upload/ChannelAdmin.png b/tests/testsources/icons/upload/ChannelAdmin.png new file mode 100644 index 0000000000000000000000000000000000000000..33d8ca9c193c17a88709c924422b58946413fd37 GIT binary patch literal 3525 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 DDW{>> literal 0 HcmV?d00001 diff --git a/tests/testsources/icons/upload/Registered.png b/tests/testsources/icons/upload/Registered.png new file mode 100644 index 0000000000000000000000000000000000000000..6a781dfb3c707a8e9139b939117200e4f9a09758 GIT binary patch literal 3431 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 z z5;iwBl;W5;-fo)s?6fN%90QrwPNitX+#CxFN(q|@nvz=W{bJD)x|3uZVBpYe1{$Wg zzmyXM6bk-I>OZmtusk(ocK?1u`TTanuDv6mTq+v>->U!6PR?)2B1vz-Q?ctIvZb2IbvW)l_?gfU`r6N_f5RjZjygUXt?dFgzO#K&LM zvu{KkTb(MJiOI!?^2n2PurtkT9~$yV&(PCK`J6u|zf{bP+u1*2V`YJK(-$9&kKJB# znwh~do+{BXT z#!pr{10DT)w1*)Rljbjsxq3D4+9sD**g$5>cpd1*?h4roiv@!37G$%_+GlPnU6?ji z;QIH`^LkaR<4-2LNxD{b!!ejqm^f002ov JPDHLkV1gEBbmRa4 literal 0 HcmV?d00001 From 751e9aace4f8b16ca5f1fdc41f1e0387c2421302 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Mon, 27 Jul 2026 17:42:47 +0200 Subject: [PATCH 13/24] update: code coverage-badge.svg --- doc/coverage/coverage-badge.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/coverage/coverage-badge.svg b/doc/coverage/coverage-badge.svg index eeca55cb..8a8675a1 100644 --- a/doc/coverage/coverage-badge.svg +++ b/doc/coverage/coverage-badge.svg @@ -14,6 +14,6 @@ coverage - 65% + 68% \ No newline at end of file From 33b26d402ef13af10de772ad5ff2fe4402738594 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 17:36:13 +0200 Subject: [PATCH 14/24] update: icon file transfer via servergroups --- src/Node/Group.php | 5 +++-- src/Node/Server.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Node/Group.php b/src/Node/Group.php index 7bfbaa8b..ece6a7ed 100644 --- a/src/Node/Group.php +++ b/src/Node/Group.php @@ -63,9 +63,10 @@ public function iconDownload() } $download = $this->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->iconGetName('iconid')); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port']); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); + $transfer = TeamSpeak3::factory($this->getParent()->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** diff --git a/src/Node/Server.php b/src/Node/Server.php index fd5c56c5..120949ef 100644 --- a/src/Node/Server.php +++ b/src/Node/Server.php @@ -2255,7 +2255,7 @@ public function transferStop(int $serverftfid, bool $delete = false): void * @return string * @throws ServerQueryException */ - protected function buildFileTransferUri(array $transfer): string + public function buildFileTransferUri(array $transfer): string { $host = $transfer['host'] ?? $transfer['ip'] ?? $this->getParent()->getAdapterHost(); $port = $transfer['port'] ?? null; @@ -2378,9 +2378,10 @@ public function iconDownload(string $iconname = null) } $download = $this->transferInitDownload(rand(0x0000, 0xFFFF), 0, $name); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); $transfer = TeamSpeak3::factory($this->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** From 2c17093708b6e00d8cf0d98059f8c82cfe0c8ecf Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 17:37:16 +0200 Subject: [PATCH 15/24] add: test for icon file transfer via group icon --- tests/DevLiveServer/FileTransferTest.php | 107 ++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index 84233fcc..e5669695 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -6,8 +6,10 @@ use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\FileTransferException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException; +use PlanetTeamSpeak\TeamSpeak3Framework\Exception\NodeException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException; +use PlanetTeamSpeak\TeamSpeak3Framework\Node\Server; use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3; class FileTransferTest extends TestCase @@ -34,6 +36,8 @@ class FileTransferTest extends TestCase private string $testPath = DIRECTORY_SEPARATOR.'tests\testsources'; + private int $sgid; + public function setUp(): void { //proof test active @@ -185,7 +189,7 @@ public function test_can_icon_download(): void $this->assertNotNull($content); - $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$icon['name']; + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$icon['name'].'.png'; file_put_contents($targetFile, $content->toString()); $this->assertFileExists($targetFile); @@ -218,4 +222,105 @@ public function test_can_icon_delete(): void $ts3_Host->getAdapter()->getTransport()->disconnect(); } + + /** + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws NodeException + * @throws FileTransferException + * @throws HelperException + */ + public function test_can_upload_servergroup_icon(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + $this->set_play_test_servergroup($ts3_VirtualServer); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Registered.png'; + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $serverGroup = $ts3_VirtualServer->serverGroupGetById($this->sgid); + $serverGroup->permAssign(['i_icon_id'], $iconId); + + $permissions = $serverGroup->permList(true); + + $this->assertArrayHasKey('i_icon_id', $permissions); + $this->assertSame($iconId, (int) $permissions['i_icon_id']['permvalue']); + + $this->unset_play_test_servergroup($ts3_VirtualServer); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + + } + + /** + * @throws HelperException + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws NodeException + * @throws FileTransferException + */ + public function test_can_download_servergroup_icon(): void + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + $this->set_play_test_servergroup($ts3_VirtualServer); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Registered.png'; + $this->assertFileExists($iconFile); + $this->assertGreaterThan(0, filesize($iconFile)); + + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $iconName = 'icon_'.$iconId; + + $serverGroup = $ts3_VirtualServer->serverGroupGetById($this->sgid); + $serverGroup->permAssign(['i_icon_id'], $iconId); + + $ts3_VirtualServer->serverGroupListReset(); + $serverGroup = $ts3_VirtualServer->serverGroupGetById($this->sgid); + $content = $serverGroup->iconDownload(); + + $this->assertNotNull($content); + $this->assertGreaterThan(0, strlen($content->toString())); + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$iconName.'.png'; + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + + $ts3_VirtualServer->iconDelete($iconId); + $this->unset_play_test_servergroup($ts3_VirtualServer); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); + } + + /** + * @throws AdapterException + * @throws TransportException + * @throws ServerQueryException + */ + private function set_play_test_servergroup(Server $ts3VirtualServer): void + { + $this->sgid = $ts3VirtualServer->serverGroupCreate('UnitTest', 1); + } + + /** + * @param Server $ts3_VirtualServer + * @throws AdapterException + * @throws ServerQueryException + * @throws TransportException + */ + public function unset_play_test_servergroup(Server $ts3_VirtualServer): void + { + $ts3_VirtualServer->serverGroupDelete($this->sgid); + } } From b4b0d3d148d55dc33a3d6cd3c8d2b676ed0278c8 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 20:21:26 +0200 Subject: [PATCH 16/24] add: test for icon file transfer via channel icon --- src/Node/Channel.php | 5 +- tests/DevLiveServer/FileTransferTest.php | 116 ++++++++++++++++++++++- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/Node/Channel.php b/src/Node/Channel.php index 79e2595c..96af37e6 100644 --- a/src/Node/Channel.php +++ b/src/Node/Channel.php @@ -455,9 +455,10 @@ public function iconDownload() } $download = $this->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->iconGetName('channel_icon_id')); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port']); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); + $transfer = TeamSpeak3::factory($this->getParent()->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index e5669695..5ec07ddc 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -38,6 +38,10 @@ class FileTransferTest extends TestCase private int $sgid; + private string $ts3_unit_test_channel_name; + + private int $test_cid; + public function setUp(): void { //proof test active @@ -50,6 +54,7 @@ public function setUp(): void $this->user = str_replace('DEV_LIVE_SERVER_QUERY_USER=', '', preg_replace('#\n(?!\n)#', '', $env[5])); $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); + $this->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7])); } else { $this->active = 'false'; } @@ -252,7 +257,6 @@ public function test_can_upload_servergroup_icon(): void $this->unset_play_test_servergroup($ts3_VirtualServer); $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); - } /** @@ -303,6 +307,91 @@ public function test_can_download_servergroup_icon(): void $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); } + /** + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws FileTransferException + * @throws HelperException + */ + public function test_can_upload_channel_icon() + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + $cid = $this->set_play_test_channel($ts3_VirtualServer); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Guest.png'; + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $signedIconId = $iconId > 0x7FFFFFFF ? $iconId - 0x100000000 : $iconId; + + $channel = $ts3_VirtualServer->channelGetById($cid); + $channel->permAssign(['i_icon_id'], $signedIconId); + + $permissions = $channel->permList(true); + + $this->assertArrayHasKey('i_icon_id', $permissions); + $this->assertSame($signedIconId, (int) $permissions['i_icon_id']['permvalue']); + + $this->unset_play_test_channel($ts3_VirtualServer); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + } + + /** + * @throws HelperException + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws FileTransferException + */ + public function test_can_download_channel_icon() + { + if ($this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + $cid = $this->set_play_test_channel($ts3_VirtualServer); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Guest.png'; + $this->assertFileExists($iconFile); + $this->assertGreaterThan(0, filesize($iconFile)); + + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $signedIconId = $iconId > 0x7FFFFFFF ? $iconId - 0x100000000 : $iconId; + $iconName = 'icon_'.$iconId; + + $channel = $ts3_VirtualServer->channelGetById($cid); + $channel->permAssign(['i_icon_id'], $signedIconId); + + $permissions = $channel->permList(true); + + $this->assertArrayHasKey('i_icon_id', $permissions); + $this->assertSame($signedIconId, (int) $permissions['i_icon_id']['permvalue']); + + $ts3_VirtualServer->channelListReset(); + $channel = $ts3_VirtualServer->channelGetById($cid); + $content = $channel->iconDownload(); + + $this->assertNotNull($content); + $this->assertGreaterThan(0, strlen($content->toString())); + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$iconName.'.png'; + + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + + $ts3_VirtualServer->iconDelete($iconId); + $this->unset_play_test_channel($ts3_VirtualServer); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); + } + /** * @throws AdapterException * @throws TransportException @@ -323,4 +412,29 @@ public function unset_play_test_servergroup(Server $ts3_VirtualServer): void { $ts3_VirtualServer->serverGroupDelete($this->sgid); } + + /** + * @throws AdapterException + * @throws TransportException + * @throws ServerQueryException + */ + private function set_play_test_channel(Server $ts3VirtualServer): int + { + $cid = $ts3VirtualServer->channelGetByName($this->ts3_unit_test_channel_name)->getId(); + + $createdCID = $ts3VirtualServer->channelCreate(['channel_name' => 'Play-Test', 'channel_flag_permanent' => 1, 'cpid' => $cid]); + $this->test_cid = $createdCID; + + return $createdCID; + } + + /** + * @throws AdapterException + * @throws ServerQueryException + * @throws HelperException + */ + public function unset_play_test_channel($ts3_VirtualServer): void + { + $ts3_VirtualServer->channelDelete($this->test_cid, true); + } } From a87bc2c9db16900a74bd5202fe7b533ed160de55 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 20:45:29 +0200 Subject: [PATCH 17/24] add: test for icon file transfer via client icon --- src/Node/Client.php | 5 +- tests/DevLiveServer/FileTransferTest.php | 59 +++++++++++++++++++++++ tests/testsources/icons/upload/Voice.png | Bin 0 -> 3542 bytes 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/testsources/icons/upload/Voice.png diff --git a/src/Node/Client.php b/src/Node/Client.php index 0c577dde..3c6d1876 100644 --- a/src/Node/Client.php +++ b/src/Node/Client.php @@ -476,9 +476,10 @@ public function iconDownload() } $download = $this->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->iconGetName('client_icon_id')); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port']); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); + $transfer = TeamSpeak3::factory($this->getParent()->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index 5ec07ddc..cb10a499 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -42,6 +42,10 @@ class FileTransferTest extends TestCase private int $test_cid; + private string $user_test_active; + + private string $ts3_unit_test_userName; + public function setUp(): void { //proof test active @@ -55,6 +59,8 @@ public function setUp(): void $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); $this->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7])); + $this->user_test_active = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_ACTIVE=', '', preg_replace('#\n(?!\n)#', '', $env[8])); + $this->ts3_unit_test_userName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER=', '', preg_replace('#\n(?!\n)#', '', $env[9])); } else { $this->active = 'false'; } @@ -392,6 +398,59 @@ public function test_can_download_channel_icon() $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); } + /** + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws FileTransferException + * @throws HelperException + */ + public function test_can_download_client_icon() + { + if ($this->user_test_active == 'false' || $this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Voice.png'; + $this->assertFileExists($iconFile); + $this->assertGreaterThan(0, filesize($iconFile)); + + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $signedIconId = $iconId > 0x7FFFFFFF ? $iconId - 0x100000000 : $iconId; + $iconName = 'icon_'.$iconId; + + $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); + $client->permAssign(['i_icon_id'], $signedIconId); + + $permissions = $client->permList(true); + + $this->assertArrayHasKey('i_icon_id', $permissions); + $this->assertSame($signedIconId, (int) $permissions['i_icon_id']['permvalue']); + + $ts3_VirtualServer->clientListReset(); + $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); + $content = $client->iconDownload(); + + $this->assertNotNull($content); + $this->assertGreaterThan(0, strlen($content->toString())); + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$iconName.'.png'; + + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + + $client->permRemove(['i_icon_id']); + $ts3_VirtualServer->iconDelete($iconId); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + + $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); + } + /** * @throws AdapterException * @throws TransportException diff --git a/tests/testsources/icons/upload/Voice.png b/tests/testsources/icons/upload/Voice.png new file mode 100644 index 0000000000000000000000000000000000000000..39d33b749bd3040e91ee7482d77dbf2eb1848c47 GIT binary patch literal 3542 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 Qod5s;07*qoM6N<$f;t+HX8-^I literal 0 HcmV?d00001 From 33e604a2c252bb375dc4e4db6aa7b01408540b8d Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 21:28:18 +0200 Subject: [PATCH 18/24] add: dev_reset_test_channel --- tests/DevLiveServer/ChannelTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/DevLiveServer/ChannelTest.php b/tests/DevLiveServer/ChannelTest.php index 4b6e9eca..e6fdcdee 100644 --- a/tests/DevLiveServer/ChannelTest.php +++ b/tests/DevLiveServer/ChannelTest.php @@ -770,4 +770,14 @@ protected function onNotSuccessfulTest(Throwable $t): never parent::onNotSuccessfulTest($t); } + + /** + * @throws AdapterException + * @throws TransportException + * @throws ServerQueryException + */ + public function dev_reset_test_channel(Server $ts3VirtualServer): void + { + $ts3VirtualServer->channelGetByName('Play-Test')->delete(); + } } From d3e95cec65b7b08d8a5615b6e7d0cc4895cd4788 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 21:38:53 +0200 Subject: [PATCH 19/24] fix: move icon client transfer to ClientTest.php --- tests/DevLiveServer/ClientTest.php | 56 ++++++++++++++++++++++ tests/DevLiveServer/FileTransferTest.php | 59 ------------------------ 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/tests/DevLiveServer/ClientTest.php b/tests/DevLiveServer/ClientTest.php index fdb9518d..1c1c94fc 100644 --- a/tests/DevLiveServer/ClientTest.php +++ b/tests/DevLiveServer/ClientTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException; +use PlanetTeamSpeak\TeamSpeak3Framework\Exception\FileTransferException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\NodeException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException; @@ -45,6 +46,8 @@ class ClientTest extends TestCase private int $cgid; + private string $testPath = DIRECTORY_SEPARATOR.'tests\testsources'; + public function setUp(): void { //proof test active @@ -668,6 +671,59 @@ public function test_has_overwolf() $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); } + /** + * @throws TransportException + * @throws ServerQueryException + * @throws AdapterException + * @throws FileTransferException + * @throws HelperException + */ + public function test_can_download_client_icon() + { + if ($this->user_test_active == 'false' || $this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + + $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Voice.png'; + $this->assertFileExists($iconFile); + $this->assertGreaterThan(0, filesize($iconFile)); + + $iconId = $ts3_VirtualServer->iconUpload($iconFile); + $signedIconId = $iconId > 0x7FFFFFFF ? $iconId - 0x100000000 : $iconId; + $iconName = 'icon_'.$iconId; + + $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); + $client->permAssign(['i_icon_id'], $signedIconId); + + $permissions = $client->permList(true); + + $this->assertArrayHasKey('i_icon_id', $permissions); + $this->assertSame($signedIconId, (int) $permissions['i_icon_id']['permvalue']); + + $ts3_VirtualServer->clientListReset(); + $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); + $content = $client->iconDownload(); + + $this->assertNotNull($content); + $this->assertGreaterThan(0, strlen($content->toString())); + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$iconName.'.png'; + + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + + $client->permRemove(['i_icon_id']); + $ts3_VirtualServer->iconDelete($iconId); + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + + $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); + } + /** * @throws AdapterException * @throws ServerQueryException diff --git a/tests/DevLiveServer/FileTransferTest.php b/tests/DevLiveServer/FileTransferTest.php index cb10a499..5ec07ddc 100644 --- a/tests/DevLiveServer/FileTransferTest.php +++ b/tests/DevLiveServer/FileTransferTest.php @@ -42,10 +42,6 @@ class FileTransferTest extends TestCase private int $test_cid; - private string $user_test_active; - - private string $ts3_unit_test_userName; - public function setUp(): void { //proof test active @@ -59,8 +55,6 @@ public function setUp(): void $this->password = str_replace('DEV_LIVE_SERVER_QUERY_USER_PASSWORD=', '', preg_replace('#\n(?!\n)#', '', $env[6])); $this->serverPort = str_replace('DEV_LIVE_SERVER_UNIT_TEST_SERVER_PORT=', '', preg_replace('#\n(?!\n)#', '', $env[12])); $this->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7])); - $this->user_test_active = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_ACTIVE=', '', preg_replace('#\n(?!\n)#', '', $env[8])); - $this->ts3_unit_test_userName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER=', '', preg_replace('#\n(?!\n)#', '', $env[9])); } else { $this->active = 'false'; } @@ -398,59 +392,6 @@ public function test_can_download_channel_icon() $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); } - /** - * @throws TransportException - * @throws ServerQueryException - * @throws AdapterException - * @throws FileTransferException - * @throws HelperException - */ - public function test_can_download_client_icon() - { - if ($this->user_test_active == 'false' || $this->active == 'false') { - $this->markTestSkipped('DevLiveServer ist not active'); - } - - $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); - - $iconFile = getcwd().DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'testsources'.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.'Voice.png'; - $this->assertFileExists($iconFile); - $this->assertGreaterThan(0, filesize($iconFile)); - - $iconId = $ts3_VirtualServer->iconUpload($iconFile); - $signedIconId = $iconId > 0x7FFFFFFF ? $iconId - 0x100000000 : $iconId; - $iconName = 'icon_'.$iconId; - - $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); - $client->permAssign(['i_icon_id'], $signedIconId); - - $permissions = $client->permList(true); - - $this->assertArrayHasKey('i_icon_id', $permissions); - $this->assertSame($signedIconId, (int) $permissions['i_icon_id']['permvalue']); - - $ts3_VirtualServer->clientListReset(); - $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); - $content = $client->iconDownload(); - - $this->assertNotNull($content); - $this->assertGreaterThan(0, strlen($content->toString())); - - $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; - $targetFile = $downloadPath.DIRECTORY_SEPARATOR.$iconName.'.png'; - - file_put_contents($targetFile, $content->toString()); - - $this->assertFileExists($targetFile); - $this->assertGreaterThan(0, filesize($targetFile)); - - $client->permRemove(['i_icon_id']); - $ts3_VirtualServer->iconDelete($iconId); - $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); - - $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); - } - /** * @throws AdapterException * @throws TransportException From dc0a2f4e6642ee269c7772933aa62aaefb02b242 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Tue, 28 Jul 2026 21:44:05 +0200 Subject: [PATCH 20/24] update: coverag-badge.svg --- doc/coverage/coverage-badge.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/coverage/coverage-badge.svg b/doc/coverage/coverage-badge.svg index 8a8675a1..e7746bf0 100644 --- a/doc/coverage/coverage-badge.svg +++ b/doc/coverage/coverage-badge.svg @@ -14,6 +14,6 @@ coverage - 68% + 69% \ No newline at end of file From 7d2d33f3f601ece1733fa86d206f77bda9785b1e Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Wed, 29 Jul 2026 17:16:34 +0200 Subject: [PATCH 21/24] chore: update dependencies --- composer.lock | 194 ++++++++++++++++++++++++-------------------------- 1 file changed, 95 insertions(+), 99 deletions(-) diff --git a/composer.lock b/composer.lock index 3dddff5f..fd3cbc70 100644 --- a/composer.lock +++ b/composer.lock @@ -127,16 +127,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.52", + "version": "3.0.55", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db9744e6d47e742b1f974e965ad49bdd041105af", + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af", "shasum": "" }, "require": { @@ -217,7 +217,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.55" }, "funding": [ { @@ -233,7 +233,7 @@ "type": "tidelift" } ], - "time": "2026-04-27T07:02:15+00:00" + "time": "2026-06-14T23:24:10+00:00" } ], "packages-dev": [ @@ -303,28 +303,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -362,7 +363,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -372,13 +373,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -702,16 +699,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.5", + "version": "v3.95.17", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7f86d8763063f5d2e2e2d0e1e45bb2f15895361d" + "reference": "0ee88422118f3cc59c8c3def222ba7f1493b6d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7f86d8763063f5d2e2e2d0e1e45bb2f15895361d", - "reference": "7f86d8763063f5d2e2e2d0e1e45bb2f15895361d", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/0ee88422118f3cc59c8c3def222ba7f1493b6d5b", + "reference": "0ee88422118f3cc59c8c3def222ba7f1493b6d5b", "shasum": "" }, "require": { @@ -745,16 +742,16 @@ "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.11.0", "infection/infection": "^0.32.7", - "justinrainbow/json-schema": "^6.8.0", + "justinrainbow/json-schema": "^6.10.0", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", - "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", - "symfony/polyfill-php85": "^1.37", - "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.11" + "phpunit/phpunit": "^9.6.35 || ^10.5.64 || ^11.5.56 || ^12.5.31 || ^13.0.6", + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.1", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -795,7 +792,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.5" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.17" }, "funding": [ { @@ -803,7 +800,7 @@ "type": "github" } ], - "time": "2026-06-09T14:55:16+00:00" + "time": "2026-07-24T13:54:39+00:00" }, { "name": "myclabs/deep-copy", @@ -867,20 +864,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -919,9 +915,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "phar-io/manifest", @@ -1388,24 +1384,24 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.29", + "version": "12.5.33", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9aa66a47db3ea70f1a468e66dd969f67e594945a" + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9aa66a47db3ea70f1a468e66dd969f67e594945a", - "reference": "9aa66a47db3ea70f1a468e66dd969f67e594945a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -1466,7 +1462,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" }, "funding": [ { @@ -1474,7 +1470,7 @@ "type": "other" } ], - "time": "2026-06-04T06:14:42+00:00" + "time": "2026-07-28T13:58:09+00:00" }, { "name": "psr/container", @@ -3118,16 +3114,16 @@ }, { "name": "symfony/console", - "version": "v7.4.13", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + "reference": "088ec6fe0ef6819cbc301174093b6bfa4ad26930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "url": "https://api.github.com/repos/symfony/console/zipball/088ec6fe0ef6819cbc301174093b6bfa4ad26930", + "reference": "088ec6fe0ef6819cbc301174093b6bfa4ad26930", "shasum": "" }, "require": { @@ -3192,7 +3188,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.13" + "source": "https://github.com/symfony/console/tree/v7.4.15" }, "funding": [ { @@ -3212,20 +3208,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:56:14+00:00" + "time": "2026-07-27T13:51:00+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -3263,7 +3259,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -3283,20 +3279,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.9", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/336e7f3b9e95aba04f93ea9143920c2186abfbb9", + "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9", "shasum": "" }, "require": { @@ -3348,7 +3344,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.15" }, "funding": [ { @@ -3368,20 +3364,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-07-21T15:13:06+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -3428,7 +3424,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -3448,20 +3444,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.11", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79", + "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79", "shasum": "" }, "require": { @@ -3498,7 +3494,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + "source": "https://github.com/symfony/filesystem/tree/v7.4.15" }, "funding": [ { @@ -3518,20 +3514,20 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:38:44+00:00" + "time": "2026-07-22T07:36:05+00:00" }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "13b38720174286f55d1761152b575a8d1436fc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", + "reference": "13b38720174286f55d1761152b575a8d1436fc25", "shasum": "" }, "require": { @@ -3566,7 +3562,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.14" }, "funding": [ { @@ -3586,7 +3582,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/options-resolver", @@ -3744,16 +3740,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -3802,7 +3798,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -3822,7 +3818,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -4305,16 +4301,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -4368,7 +4364,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -4388,7 +4384,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/stopwatch", @@ -4458,16 +4454,16 @@ }, { "name": "symfony/string", - "version": "v7.4.13", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + "reference": "e394af32256bf9e7bf80849d95e589167c10097b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b", "shasum": "" }, "require": { @@ -4525,7 +4521,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.13" + "source": "https://github.com/symfony/string/tree/v7.4.15" }, "funding": [ { @@ -4545,7 +4541,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T15:23:29+00:00" + "time": "2026-07-28T07:33:02+00:00" }, { "name": "theseer/tokenizer", From 11e58090754896368aca6d0ac18ee0c93784f8b5 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Wed, 29 Jul 2026 17:23:04 +0200 Subject: [PATCH 22/24] chore: understood, cleanup --- src/Node/ServerGroup.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Node/ServerGroup.php b/src/Node/ServerGroup.php index 03470aba..fdee024e 100644 --- a/src/Node/ServerGroup.php +++ b/src/Node/ServerGroup.php @@ -206,7 +206,6 @@ protected function fetchNodeList(): void */ public function getIcon(): string { - //TODO: HÄ whats this? return 'group_server'; } } From 1ff79655a45f905733b071686d4f75c4a0eebdff Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Sun, 2 Aug 2026 12:06:02 +0200 Subject: [PATCH 23/24] fix: download client avatar --- src/Node/Client.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Node/Client.php b/src/Node/Client.php index 3c6d1876..4feda7ec 100644 --- a/src/Node/Client.php +++ b/src/Node/Client.php @@ -374,9 +374,10 @@ public function avatarDownload() } $download = $this->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->avatarGetName()); - $transfer = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port']); + $ftkey = str_replace('\\/', '/', (string) $download['ftkey']); + $transfer = TeamSpeak3::factory($this->getParent()->buildFileTransferUri($download)); - return $transfer->download($download['ftkey'], $download['size']); + return $transfer->download($ftkey, (int) $download['size']); } /** From eeda9021eaa3b09cc00d2567b7640da8d6c8b895 Mon Sep 17 00:00:00 2001 From: Oliver Nitzsche Date: Sun, 2 Aug 2026 12:06:40 +0200 Subject: [PATCH 24/24] add: test download client avatar add: test download client avatar --- tests/DevLiveServer/ClientTest.php | 36 ++++++++++++++++++ .../icons/upload/Avatar-default-512.png | Bin 0 -> 17972 bytes 2 files changed, 36 insertions(+) create mode 100644 tests/testsources/icons/upload/Avatar-default-512.png diff --git a/tests/DevLiveServer/ClientTest.php b/tests/DevLiveServer/ClientTest.php index 1c1c94fc..da853cf0 100644 --- a/tests/DevLiveServer/ClientTest.php +++ b/tests/DevLiveServer/ClientTest.php @@ -724,6 +724,42 @@ public function test_can_download_client_icon() $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); } + /** + * @throws AdapterException + * @throws TransportException + * @throws ServerQueryException + * @throws FileTransferException + * @throws HelperException + * @throws \Exception + */ + public function test_can_download_client_avatar() + { + if ($this->user_test_active == 'false' || $this->active == 'false') { + $this->markTestSkipped('DevLiveServer ist not active'); + } + + $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri); + $client = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName); + $content = $client->avatarDownload(); + + if ($content === null) { + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + $this->markTestIncomplete('Test client has no avatar set'); + } + + $this->assertGreaterThan(0, strlen($content->toString())); + + $downloadPath = getcwd().$this->testPath.DIRECTORY_SEPARATOR.'icons'.DIRECTORY_SEPARATOR.'download'; + $targetFile = $downloadPath.DIRECTORY_SEPARATOR.'avatar_'.$client['client_database_id'].'.png'; + file_put_contents($targetFile, $content->toString()); + + $this->assertFileExists($targetFile); + $this->assertGreaterThan(0, filesize($targetFile)); + + $ts3_VirtualServer->getAdapter()->getTransport()->disconnect(); + $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected()); + } + /** * @throws AdapterException * @throws ServerQueryException diff --git a/tests/testsources/icons/upload/Avatar-default-512.png b/tests/testsources/icons/upload/Avatar-default-512.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a891685639ec4e754d20b1cffcf8360cff89ea GIT binary patch literal 17972 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