Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b5f9851
add testPreventConvertIntToUtf8
MajorOli Jan 4, 2025
dacd643
update testPreventConvertIntToUtf8 with value range which cause the e…
MajorOli Jan 4, 2025
4d033d7
update testPreventConvertIntToUtf8 with value range which cause the e…
MajorOli Jan 4, 2025
14fd733
fix: Convert::version returns an Object so we need the toString() met…
MajorOli Jan 4, 2025
cb80967
1 test triggered 1 PHP deprecation:
MajorOli Jan 4, 2025
ef0ee63
add format for negative time / seconds
MajorOli Jan 4, 2025
7a5bfe9
Merge pull request #226 from MajorOli/dev
Sebbo94BY Sep 21, 2025
3f9778d
fix: recognize ASCII strings as UTF-8
Sep 16, 2026
4b84fca
Merge pull request #231 from planetteamspeak/fix/utf8-numeric-strings
Sebbo94BY Sep 16, 2026
11a2538
fix: Issue #209: Preserve partial non-blocking SSH responses
Sep 16, 2026
7abe834
fix: normalize group sort IDs before comparison
Sep 16, 2026
bbe36ae
Merge pull request #232 from planetteamspeak/fix/group-sort-stringhelper
Sebbo94BY Sep 16, 2026
077cca4
fix: disconnect transport before adapter shutdown
Sep 16, 2026
4eb9917
Merge pull request #233 from planetteamspeak/fix/transport-destructor…
Sebbo94BY Sep 16, 2026
3905577
fix: update Composer installation examples
Sep 16, 2026
653d8d4
Merge pull request #234 from planetteamspeak/fix/issue-223-composer-a…
Sebbo94BY Sep 16, 2026
4d1c7fd
Merge pull request #230 from planetteamspeak/Issue-209-SSH-connection…
Sebbo94BY Sep 16, 2026
dfb27bb
build: require PHP 8.3 and refresh dependencies
Sep 16, 2026
d916eac
test: modernize PHPUnit and remove PHP deprecations
Sep 16, 2026
b387479
Merge pull request #235 from planetteamspeak/feature/php83-dependencies
Sebbo94BY Sep 16, 2026
15a573d
test: make transport coverage deterministic
Sep 16, 2026
3ef6bb5
test: cover node and viewer behavior
Sep 16, 2026
83833fd
test: expand helper and query coverage
Sep 16, 2026
0d05b5b
fix: skip inaccessible channels in viewers
Sep 16, 2026
aa4627e
Merge pull request #236 from planetteamspeak/feature/strengthen-unit-…
Sebbo94BY Sep 16, 2026
dff6bc8
feat: support GreenTeaSpeak protocol greetings
Sep 16, 2026
2f08bf7
Merge pull request #237 from planetteamspeak/feature/greenteaspeak-pr…
Sebbo94BY Sep 16, 2026
a1a7e7f
fix: correct channel child lookups
Sep 16, 2026
4b162ec
fix: detect failed transport writes
Sep 16, 2026
294fe10
fix: account for partial file transfer reads
Sep 16, 2026
e952ed1
fix: release adapter profiler timers
Sep 16, 2026
00c24e7
fix: keep reply parsing state immutable
Sep 16, 2026
76b42aa
fix: validate complete UTF-8 strings
Sep 16, 2026
24f2a71
feat: bound non-blocking reads and add TLS verification
Sep 16, 2026
dc1c221
fix: return channel clients by ID
Sep 16, 2026
e659e06
fix: retain supported 64-bit query integers
Sep 16, 2026
eb40e10
fix: preserve falsy URI parameter values
Sep 16, 2026
1ed44bc
feat: clear all signal handlers
Sep 16, 2026
ceab29e
fix: apply node filters to every rule
Sep 16, 2026
9479019
feat: stream file transfer downloads
Sep 16, 2026
ec3af91
fix: close failed and disconnected transports
Sep 16, 2026
62dec40
fix: parse URI query variables consistently
Sep 16, 2026
3e42b68
fix: use collision-resistant transfer identifiers
Sep 16, 2026
700731c
fix: escape HTML viewer image attributes
Sep 16, 2026
9acb665
fix: reject malformed encoded helper input
Sep 16, 2026
f9fd7e4
Merge pull request #238 from planetteamspeak/fix/core-correctness-and…
Sebbo94BY Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/phpcsfixer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ jobs:
fail-fast: false
matrix:
php_versions: [
'8.3',
'8.5',
]
name: PHP ${{ matrix.php_versions }}
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: install php ${{ matrix.php_versions }} with extensions
uses: shivammathur/setup-php@v2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ jobs:
fail-fast: false
matrix:
php_versions: [
'8.1',
'8.2',
'8.3',
'8.4',
'8.5',
]
name: PHP ${{ matrix.php_versions }}
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: install php ${{ matrix.php_versions }} with extensions
uses: shivammathur/setup-php@v2
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Speed up new development and reduce maintenance costs by using this nifty piece

Note that the majority of TS3 PHP Framework development and deployment is done on nginx, so there is more community experience and testing performed on Apache than on other web servers.

You can install the TS3 PHP Framework by [manually downloading](https://github.com/planetteamspeak/ts3phpframework/archive/refs/heads/master.zip) it or using Composer.
Install the TS3 PHP Framework with [Composer](https://getcomposer.org/). Composer generates the PSR-4 autoloader required by the framework.

Install the latest available release:

Expand Down Expand Up @@ -143,10 +143,10 @@ $uri = "serverquery://username:password@[fe80::250:56ff:fe16:1447]:10022/?ssh=1"

#### SSL/TLS Connections ([TeaSpeak Server](https://www.teaspeak.de) only)

Secure ServerQuery connections can be established using the optional `tls` parameter:
Secure TeaSpeak ServerQuery connections can be established using the optional `tls` parameter. Certificate verification remains disabled by default for compatibility with self-signed deployments. Enable `tls_verify` only when the server certificate is trusted by PHP and matches the hostname:

```php
$uri = "serverquery://username:password@[fe80::250:56ff:fe16:1447]:10011/?tls=1";
$uri = "serverquery://username:password@teaspeak.example.com:10011/?tls=1&tls_verify=1";
```

#### Custom Protocol Welcome Message and/or MOTD ([TeaSpeak Server](https://www.teaspeak.de) only)
Expand Down Expand Up @@ -187,7 +187,7 @@ Additional:
* [RFC 2396 - Section 3](https://tools.ietf.org/html/rfc2396#section-3.4) - Valid URI syntax (specifically, components of)
* [PHP.net - rawurlencode](http://us2.php.net/manual/en/function.rawurlencode.php)

#### Usual PHP Code (`require` solution)
#### Usual PHP Code
Usual PHP code means a simple created `file.php`, where you start writing your code like this:

```php
Expand All @@ -200,9 +200,12 @@ When you use this solution, you'll probably start using the TS3 PHP Framework l

```php
<?php
// load framework files
require_once("libraries/TeamSpeak3/TeamSpeak3.php");

// load Composer dependencies and the framework's PSR-4 classes
require_once __DIR__ . "/vendor/autoload.php";

use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TeamSpeak3Exception;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;

try
{
// IPv4 connection URI
Expand All @@ -214,7 +217,7 @@ try
// spawn an object for the channel using a specified name
$ts3_Channel = $ts3_VirtualServer->channelGetByName("I do not exist");
}
catch(TeamSpeak3_Exception $e)
catch(TeamSpeak3Exception $e)
{
// print the error message returned by the server
echo "Error " . $e->getCode() . ": " . $e->getMessage();
Expand All @@ -229,7 +232,7 @@ When you use a MVC based software like Symfony, CakePHP, Laravel or something si
<?php

use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3_Exception;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TeamSpeak3Exception;

class TeamspeakController extends Controller
{
Expand All @@ -240,16 +243,13 @@ class TeamspeakController extends Controller
// IPv4 connection URI
$uri = "serverquery://username:password@127.0.0.1:10011/?server_port=9987";

// Create new object of TS3 PHP Framework class
$TS3PHPFramework = new TeamSpeak3();

// connect to above specified server, authenticate and spawn an object for the virtual server on port 9987
$ts3_VirtualServer = $TS3PHPFramework->factory($uri);
$ts3_VirtualServer = TeamSpeak3::factory($uri);

// spawn an object for the channel using a specified name
$ts3_Channel = $ts3_VirtualServer->channelGetByName("I do not exist");
}
catch(TeamSpeak3_Exception $e)
catch(TeamSpeak3Exception $e)
{
// print the error message returned by the server
return "Error " . $e->getCode() . ": " . $e->getMessage();
Expand Down
18 changes: 11 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Modern use-at-will framework that provides individual components to manage TeamSpeak 3 Server instances",
"keywords": ["ts3", "teamspeak","server","query","filetransfer","management","interface","api"],
"homepage": "https://www.planetteamspeak.com",
"license": "GPL-3.0",
"license": "MIT",
"authors": [
{
"name": "Sven 'ScP' Paulsen",
Expand All @@ -17,24 +17,28 @@
"docs": "https://docs.planetteamspeak.com/ts3/php/framework"
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"require": {
"php": ">=8.1",
"php": ">=8.3",
"ext-mbstring": "*",
"ext-json": "*",
"ext-ctype": "*",
"phpseclib/phpseclib": "^3.0",
"phpseclib/phpseclib": "^4.0",
"ext-openssl": "*"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.1",
"friendsofphp/php-cs-fixer": "^3.8",
"php-coveralls/php-coveralls": "^2.5",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^9.5",
"phpunit/phpunit": "^12.5",
"react/socket": "^1.11",
"squizlabs/php_codesniffer": "^3.6",
"symfony/yaml": "^6.0"
"symfony/yaml": "^7.4"
},
"autoload": {
"psr-4": {
Expand All @@ -47,7 +51,7 @@
}
},
"scripts": {
"compatibility": "\"vendor/bin/phpcs\" -ps --basepath=. --standard=PHPCompatibility --runtime-set testVersion 8.0- src",
"compatibility": "\"vendor/bin/phpcs\" -ps --basepath=. --standard=PHPCompatibility --runtime-set testVersion 8.3- src",
"code-style": "\"vendor/bin/php-cs-fixer\" fix --config .php-cs-fixer.php --diff",
"tests": [
"\"vendor/bin/phpunit\" --no-coverage"
Expand Down
33 changes: 17 additions & 16 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false"
backupStaticAttributes="false"
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
backupStaticProperties="false"
cacheDirectory=".phpunit.cache"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd"
>
<coverage>
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./images</directory>
<directory>./public</directory>
<directory>./tests</directory>
<directory>./build</directory>
<directory>./vendor</directory>
</exclude>
<report>
<clover outputFile="build/logs/clover.xml"/>
</report>
Expand All @@ -32,4 +21,16 @@
<logging>
<junit outputFile="build/logs/junit.xml"/>
</logging>
<source>
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./images</directory>
<directory>./public</directory>
<directory>./tests</directory>
<directory>./build</directory>
<directory>./vendor</directory>
</exclude>
</source>
</phpunit>
63 changes: 33 additions & 30 deletions src/Adapter/FileTransfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public function __destruct()
if ($this->getTransport() instanceof Transport && $this->getTransport()->isConnected()) {
$this->getTransport()->disconnect();
}

Profiler::remove(spl_object_hash($this));
}

/**
Expand Down Expand Up @@ -105,54 +107,55 @@ public function upload(string $ftkey, int $seek, string $data): void
*/
public function download(string $ftkey, int $size, bool $passthru = false)
{
$this->init($ftkey);

if ($passthru) {
$this->passthru($size);
$this->downloadTo($ftkey, $size, static function (StringHelper $data): void {
echo $data;
});
return;
}

$buff = new StringHelper("");
$pack = 4096;

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;

$this->downloadTo($ftkey, $size, static function (StringHelper $data) use ($buff): void {
$buff->append($data);

Signal::getInstance()->emit("filetransferDownloadProgress", $ftkey, count($buff), $size);
}

$this->getProfiler()->stop();

Signal::getInstance()->emit("filetransferDownloadFinished", $ftkey, count($buff), $size);

if (strlen($buff) != $size) {
throw new FileTransferException("incomplete file download (" . count($buff) . " of " . $size . " bytes)");
}
});

return $buff;
}

/**
* Outputs all remaining data on a TeamSpeak 3 file transfer stream using PHP's fpassthru()
* function.
* Downloads a file and passes each received chunk to the given consumer.
*
* @param string $ftkey
* @param integer $size
* @param callable $consumer
* @return void
* @throws FileTransferException
*/
protected function passthru(int $size): void
public function downloadTo(string $ftkey, int $size, callable $consumer): void
{
$buff_size = fpassthru($this->getTransport()->getStream());
$this->init($ftkey);
$pack = 4096;
$seek = 0;

Signal::getInstance()->emit("filetransferDownloadStarted", $ftkey, $seek, $size);

try {
while ($seek < $size) {
$data = $this->getTransport()->read(min($size - $seek, $pack));

if ($buff_size != $size) {
throw new FileTransferException("incomplete file download (" . $buff_size . " of " . $size . " bytes)");
if (count($data) === 0) {
throw new FileTransferException("incomplete file download (" . $seek . " of " . $size . " bytes)");
}

$consumer($data);
$seek += count($data);

Signal::getInstance()->emit("filetransferDownloadProgress", $ftkey, $seek, $size);
}
} finally {
$this->getProfiler()->stop();
}

Signal::getInstance()->emit("filetransferDownloadFinished", $ftkey, $seek, $size);
}
}
4 changes: 1 addition & 3 deletions src/Adapter/MockServerQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ protected function syn(): void

$rdy = $this->getTransport()->readLine();

if (!$rdy->startsWith(TeamSpeak3::TS3_PROTO_IDENT) && !$rdy->startsWith(TeamSpeak3::TEA_PROTO_IDENT) && !(defined("CUSTOM_PROTO_IDENT") && $rdy->startsWith(CUSTOM_PROTO_IDENT))) {
throw new AdapterException("invalid reply from the server (" . $rdy . ")");
}
$this->validateProtocolGreeting($rdy);

Signal::getInstance()->emit("serverqueryConnected", $this);
}
Expand Down
23 changes: 19 additions & 4 deletions src/Adapter/ServerQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,25 @@ protected function syn(): void

$rdy = $this->getTransport()->readLine();

if (!$rdy->startsWith(TeamSpeak3::TS3_PROTO_IDENT) && !$rdy->startsWith(TeamSpeak3::TEA_PROTO_IDENT) && !(defined("CUSTOM_PROTO_IDENT") && $rdy->startsWith(CUSTOM_PROTO_IDENT))) {
throw new AdapterException("invalid reply from the server (" . $rdy . ")");
}
$this->validateProtocolGreeting($rdy);

Signal::getInstance()->emit("serverqueryConnected", $this);
}

/**
* Validates a ServerQuery protocol greeting.
*
* @param StringHelper $greeting
* @return void
* @throws AdapterException
*/
protected function validateProtocolGreeting(StringHelper $greeting): void
{
if (!$greeting->startsWith(TeamSpeak3::TS3_PROTO_IDENT) && !$greeting->startsWith(TeamSpeak3::TEA_PROTO_IDENT) && !$greeting->startsWith(TeamSpeak3::GREENTEA_PROTO_IDENT) && !(defined("CUSTOM_PROTO_IDENT") && $greeting->startsWith(CUSTOM_PROTO_IDENT))) {
throw new AdapterException("invalid reply from the server (" . $greeting . ")");
}
}

/**
* The ServerQuery destructor.
*
Expand All @@ -80,16 +92,19 @@ public function __destruct()
{
// do not disconnect, when acting as bot in non-blocking mode
if (! $this->getTransport()->getConfig("blocking")) {
Profiler::remove(spl_object_hash($this));
return;
}

if ($this->getTransport() instanceof Transport && $this->transport->isConnected()) {
try {
$this->request("quit");
} catch (AdapterException) {
return;
// The transport destructor will close an unavailable connection.
}
}

Profiler::remove(spl_object_hash($this));
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Adapter/ServerQuery/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public function __construct(StringHelper $evt, Host $con = null)
throw new AdapterException("invalid notification event format");
}

list($type, $data) = $evt->split(TeamSpeak3::SEPARATOR_CELL, 2);
$parts = $evt->split(TeamSpeak3::SEPARATOR_CELL, 2);
$type = $parts[0];
$data = $parts[1] ?? null;

if (empty($data)) {
throw new AdapterException("invalid notification event data");
Expand Down
Loading
Loading