diff --git a/.env.testing.example b/.env.testing.example
index 1434c598..6f91497b 100644
--- a/.env.testing.example
+++ b/.env.testing.example
@@ -9,3 +9,4 @@ 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=
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 75edb27b..fb560a9e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,24 +1,3 @@
-# How to contribute
-
-We are open to contributions from everyone! 3rd-party support goes a long way
-towards maintaining and improving the framework. Nothing is too small, from pull
-requests to reporting bugs, everything helps!
-
## Ways to contribute
-* Having trouble or find a bug? [Open an
-issue!](https://github.com/planetteamspeak/ts3phpframework/issues/new)
-* Add a feature or fix a bug:
- * Check for existing issue or create a new one.
- * Fork the repo, make your changes.
- * Create a pull request, and reference the issue.
-* Add examples, tests, or improve documentation.
-
-## Test
-
-When committing code, please make sure to test before creating a pull request.
-
-We use PHPUnit for testing, feel free to add new tests. This is not a
-requirement, but helps us maintain code coverage.
-
-@ToDo: Add sections: test environment setup, running tests, examples
+* Having trouble or find a bug? [Open an issue!](https://github.com/Prestige-Solution/ts-x-php-framework/issues)
diff --git a/doc/coverage/coverage-badge.svg b/doc/coverage/coverage-badge.svg
index 66360ebe..eeca55cb 100644
--- a/doc/coverage/coverage-badge.svg
+++ b/doc/coverage/coverage-badge.svg
@@ -14,6 +14,6 @@
coverage
- 50%
+ 65%
\ No newline at end of file
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 7b7a8ef8..c3048af9 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -27,6 +27,14 @@
./tests
./build
./vendor
+ ./src/Exception/AdapterException.php
+ ./src/Exception/FileTransferException.php
+ ./src/Exception/HelperException.php
+ ./src/Exception/NodeException.php
+ ./src/Exception/ProfilerException.php
+ ./src/Exception/SignalException.php
+ ./src/Exception/TransportException.php
+ ./src/Helper/Signal/SignalInterface.php
char = $char;
@@ -167,7 +167,7 @@ public function toUnicode(): int
if ($h <= 0x7F) {
return $h;
} elseif ($h < 0xC2) {
- return false;
+ return -1;
} elseif ($h <= 0xDF) {
return ($h & 0x1F) << 6 | (ord($this->char[1]) & 0x3F);
} elseif ($h <= 0xEF) {
@@ -198,11 +198,18 @@ public function toHex(): string
*/
public static function fromHex(string $hex): self
{
- if (strlen($hex) != 2) {
+ // Check: only even numbers of hex characters allowed, all must be valid
+ if (strlen($hex) % 2 !== 0 || ! ctype_xdigit($hex)) {
throw new HelperException("given parameter '".$hex."' is not a valid hexadecimal number");
}
- return new self(chr(hexdec($hex)));
+ // Hex → Binary string (UTF-8 compatible)
+ $bytes = hex2bin($hex);
+ if ($bytes === false) {
+ throw new HelperException("given parameter '".$hex."' could not be converted to binary data");
+ }
+
+ return new self($bytes);
}
/**
diff --git a/src/Node/Channel.php b/src/Node/Channel.php
index 2a421130..179a67c3 100644
--- a/src/Node/Channel.php
+++ b/src/Node/Channel.php
@@ -17,10 +17,6 @@
*/
class Channel extends Node
{
- private array|null $clientList = null;
-
- private array $channelList = [];
-
/**
* Channel constructor.
*
@@ -558,16 +554,6 @@ protected function fetchNodeInfo(): void
$this->nodeInfo = array_merge($this->nodeInfo, $this->execute('channelinfo', ['cid' => $this->getId()])->toList());
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->getParent()->getUniqueId().'_ch'.$this->getId();
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Node/ChannelGroup.php b/src/Node/ChannelGroup.php
index c7da5c23..e5cfd564 100644
--- a/src/Node/ChannelGroup.php
+++ b/src/Node/ChannelGroup.php
@@ -98,14 +98,14 @@ public function permList(bool $permsid = false): array
* Adds a set of specified permissions to the channel group. Multiple permissions
* can be added by providing the two parameters of each permission in separate arrays.
*
- * @param int $permid
- * @param int $permvalue
+ * @param int|array $permid
+ * @param int|array $permvalue
* @return void
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function permAssign(int $permid, int $permvalue): void
+ public function permAssign(int|array $permid, int|array $permvalue): void
{
$this->getParent()->channelGroupPermAssign($this->getId(), $permid, $permvalue);
}
@@ -114,13 +114,13 @@ public function permAssign(int $permid, int $permvalue): void
* Removes a set of specified permissions from the channel group. Multiple
* permissions can be removed at once.
*
- * @param int $permid
+ * @param int|array $permid
* @return void
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function permRemove(int $permid): void
+ public function permRemove(int|array $permid): void
{
$this->getParent()->channelGroupPermRemove($this->getId(), $permid);
}
@@ -172,16 +172,6 @@ protected function fetchNodeList(): void
}
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->getParent()->getUniqueId().'_cg'.$this->getId();
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Node/Client.php b/src/Node/Client.php
index 089ce267..0c577dde 100644
--- a/src/Node/Client.php
+++ b/src/Node/Client.php
@@ -293,13 +293,13 @@ public function permAssign(int|array $permid, int|array $permvalue, array|bool $
/**
* Removes a set of specified permissions from a client. Multiple permissions can be removed at once.
*
- * @param int $permid
+ * @param array|int $permid
* @return void
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function permRemove(int $permid): void
+ public function permRemove(array|int $permid): void
{
$this->getParent()->clientPermRemove($this['client_database_id'], $permid);
}
@@ -527,16 +527,6 @@ protected function fetchNodeInfo(): void
}
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->getParent()->getUniqueId().'_cl'.$this->getId();
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Node/Group.php b/src/Node/Group.php
index 67aabb6d..7bfbaa8b 100644
--- a/src/Node/Group.php
+++ b/src/Node/Group.php
@@ -23,9 +23,14 @@ abstract class Group extends Node
*/
public function message(string $msg): void
{
- foreach ($this as $client) {
+ // get all clients in this group
+ $clients = $this->getParent()->channelGroupClientList($this->getId());
+
+ // get client id from dbid and send a textmessage
+ foreach ($clients as $client) {
try {
- $this->execute('sendtextmessage', ['msg' => $msg, 'target' => $client, 'targetmode' => TeamSpeak3::TEXTMSG_CLIENT]);
+ $targetClientID = $this->getParent()->clientgetbydbid($client['cldbid'])->getId();
+ $this->execute('sendtextmessage', ['msg' => $msg, 'target' => $targetClientID, 'targetmode' => TeamSpeak3::TEXTMSG_CLIENT]);
} catch (ServerQueryException $e) {
/* ERROR_client_invalid_id */
if ($e->getCode() != 0x0200) {
diff --git a/src/Node/Host.php b/src/Node/Host.php
index e9b0af85..bda43f09 100644
--- a/src/Node/Host.php
+++ b/src/Node/Host.php
@@ -71,13 +71,26 @@ public function serverSelectedPort(): int
* @throws ServerQueryException
* @throws TransportException
*/
- public function version(string $ident = null): mixed
+ public function version(): array
{
if ($this->version === null) {
- $this->version = $this->request('version')->toList();
+ $raw = $this->request('version')->toList();
+
+ // Find the first array that contains real data
+ foreach ($raw as $item) {
+ if (is_array($item) && isset($item['version'])) {
+ $this->version = $item;
+ break;
+ }
+ }
+
+ // If no matching array was found, empty array
+ if ($this->version === null) {
+ $this->version = [];
+ }
}
- return ($ident && isset($this->version[$ident])) ? $this->version[$ident] : $this->version;
+ return $this->version;
}
/**
@@ -176,7 +189,7 @@ public function serverIdGetByPort(int $port): int
{
$sid = $this->execute('serveridgetbyport', ['virtualserver_port' => $port])->toList();
- return $sid['server_id'];
+ return $sid[1]['server_id'];
}
/**
@@ -230,34 +243,38 @@ public function serverGetByPort(int $port): Server
}
/**
+ * Return server data as array
+ *
* @param string $name
- * @return Server
+ * @return array
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function serverGetByName(string $name): Server
+ public function serverGetByName(string $name): array
{
foreach ($this->serverList() as $server) {
if ($server['virtualserver_name'] === $name) {
- return $server;
+ return $server[1];
}
}
throw new ServerQueryException('invalid serverID', 0x400);
}
/**
+ * Return server data as array
+ *
* @param string $uid
- * @return Server
+ * @return array
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function serverGetByUid(string $uid): Server
+ public function serverGetByUid(string $uid): array
{
foreach ($this->serverList() as $server) {
if ($server['virtualserver_unique_identifier'] === $uid) {
- return $server;
+ return $server[1];
}
}
throw new ServerQueryException('invalid serverID', 0x400);
@@ -472,7 +489,7 @@ public function permissionTree(): array
$permtree[$val] = [
'permcatid' => $val,
'permcathex' => '0x'.dechex($val),
- 'permcatname' => StringHelper::factory(Convert::permissionCategory($val)),
+ 'permcatname' => StringHelper::factory(Convert::permissionCategory($val))->toString(),
'permcatparent' => 0,
'permcatchilren' => 0,
'permcatcount' => 0,
@@ -505,7 +522,19 @@ public function permissionFind(int|array $permissionId): array
$permident = (is_numeric(current($permissionId))) ? 'permid' : 'permsid';
}
- return $this->execute('permfind', [$permident => $permissionId])->toArray();
+ try {
+ $result = $this->execute('permfind', [$permident => $permissionId])->toArray();
+ } catch (ServerQueryException $e) {
+ throw new ServerQueryException('invalid permission ID');
+ }
+
+ // Remove meta-entries and keep only real data
+ $filtered = array_filter($result, function ($item) {
+ return is_array($item) && ! array_key_exists('permfind', $item) && ! array_key_exists('permsid', $item);
+ });
+
+ // Return only the relevant data array (flatten)
+ return array_values($filtered);
}
/**
@@ -632,7 +661,22 @@ public function selfPermCheck(int|array $permid): array
$permident = (is_numeric(current($permid))) ? 'permid' : 'permsid';
}
- return $this->execute('permget', [$permident => $permid])->toAssocArray('permsid');
+ $result = $this->execute('permget', [$permident => $permid])->toArray();
+
+ // Remove meta entries
+ $filtered = array_filter($result, function ($item) {
+ return is_array($item) && ! array_key_exists('permget', $item) && ! isset($item['permget']);
+ });
+
+ // If there are several, take the first real one.
+ $flattened = reset($filtered);
+
+ // If available: index via permsid
+ if (isset($flattened['permsid'])) {
+ return $flattened;
+ }
+
+ return [];
}
/**
@@ -664,41 +708,41 @@ public function message(string $msg): void
$this->execute('gm', ['msg' => $msg]);
}
- /**
- * Displays a specified number of entries (1-100) from the server log.
- *
- * @param int $lines
- * @param int|null $begin_pos
- * @param bool|null $reverse
- * @param bool $instance
- * @return array
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- public function logView(int $lines = 30, int $begin_pos = null, bool $reverse = null, bool $instance = true): array
- {
- return $this->execute('logview', ['lines' => $lines, 'begin_pos' => $begin_pos, 'instance' => $instance, 'reverse' => $reverse])->toArray();
- }
-
- /**
- * Writes a custom entry into the server instance log.
- *
- * @param string $logmsg
- * @param int $loglevel
- * @return void
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- public function logAdd(string $logmsg, int $loglevel = TeamSpeak3::LOGLEVEL_INFO): void
- {
- $sid = $this->serverSelectedId();
-
- $this->serverDeselect();
- $this->execute('logadd', ['logmsg' => $logmsg, 'loglevel' => $loglevel]);
- $this->serverSelect($sid);
- }
+// /**
+// * Displays a specified number of entries (1-100) from the server log.
+// *
+// * @param int $lines
+// * @param int|null $begin_pos
+// * @param bool|null $reverse
+// * @param bool $instance
+// * @return array
+// * @throws ServerQueryException
+// */
+// public function logView(int $lines = 30, int $begin_pos = null, bool $reverse = null, bool $instance = true): array
+// {
+// //TODO: $ts3_host->logView() defined in Server.php
+// return $this->execute('logview', ['lines' => $lines, 'begin_pos' => $begin_pos, 'instance' => $instance, 'reverse' => $reverse])->toArray();
+// }
+
+// /**
+// * Writes a custom entry into the server instance log.
+// *
+// * @param string $logmsg
+// * @param int $loglevel
+// * @return void
+// * @throws AdapterException
+// * @throws ServerQueryException
+// * @throws TransportException
+// */
+// public function logAdd(string $logmsg, int $loglevel = TeamSpeak3::LOGLEVEL_INFO): void
+// {
+// //TODO: $ts3_host->logView() defined in Server.php
+// $sid = $this->serverSelectedId();
+//
+// $this->serverDeselect();
+// $this->execute('logadd', ['logmsg' => $logmsg, 'loglevel' => $loglevel]);
+// $this->serverSelect($sid);
+// }
/**
* @throws TransportException
@@ -758,14 +802,30 @@ public function logout(): void
* Returns the number of ServerQuery logins on the selected virtual server.
*
* @param string|null $pattern
- * @return mixed
+ * @return int
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function queryCountLogin(string $pattern = null): mixed
+ public function queryCountLogin(string $pattern = null): int
{
- return current($this->execute('queryloginlist -count', ['duration' => 1, 'pattern' => $pattern])->toList());
+ $result = $this->execute('queryloginlist -count', ['duration' => 1, 'pattern' => $pattern])->toAssocArray('cldbid');
+
+ // Remove meta entry
+ $filtered = array_filter($result, function ($item) {
+ return is_array($item) && ! array_key_exists('queryloginlist', $item);
+ });
+
+ // Flat array
+ $filtered = array_values($filtered);
+
+ // Extract value from the first entry
+ if (! empty($filtered) && isset($filtered[0]['count'])) {
+ return (int) $filtered[0]['count'];
+ }
+
+ // Fallback: no result
+ return 0;
}
/**
@@ -927,18 +987,36 @@ protected function fetchNodeInfo(): void
*/
protected function fetchPermissionList(): void
{
- $reply = $this->request('permissionlist -new')->toArray();
+ $raw = $this->request('permissionlist -new')->toArray();
$start = 1;
$this->permissionEnds = [];
$this->permissionList = [];
- foreach ($reply as $line) {
+ foreach ($raw as $line) {
+ // Skip meta lines
+ if (isset($line['permissionlistpermissionlist']) || isset($line['-newpermissionlist']) || isset($line['-new'])) {
+ continue;
+ }
+
+ // If group_id_end exists → save separately
if (array_key_exists('group_id_end', $line)) {
$this->permissionEnds[] = $line['group_id_end'];
- } else {
- $this->permissionList[$line['permname']->toString()] = array_merge(['permid' => $start++], $line);
+ continue;
}
+
+ // If no permname → skip
+ if (! isset($line['permname'])) {
+ continue;
+ }
+
+ // StringHelper or Convert string securely
+ $permname = is_object($line['permname']) && method_exists($line['permname'], 'toString')
+ ? $line['permname']->toString()
+ : (string) $line['permname'];
+
+ // Save permission
+ $this->permissionList[$permname] = array_merge(['permid' => $start++], $line);
}
}
@@ -948,7 +1026,7 @@ protected function fetchPermissionList(): void
protected function fetchPermissionCats(): void
{
$permcats = [];
- $reflects = new ReflectionClass('TeamSpeak3');
+ $reflects = new ReflectionClass(TeamSpeak3::class);
foreach ($reflects->getConstants() as $key => $val) {
if (! StringHelper::factory($key)->startsWith('PERM_CAT') || $val == 0xFF) {
@@ -1010,16 +1088,6 @@ public function getAdapter(): ServerQuery
return $this->getParent();
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return 'ts3_h';
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Node/Node.php b/src/Node/Node.php
index 49288d7a..2bf1d895 100644
--- a/src/Node/Node.php
+++ b/src/Node/Node.php
@@ -12,9 +12,7 @@
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Convert;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
-use PlanetTeamSpeak\TeamSpeak3Framework\Viewer\ViewerInterface;
use RecursiveIterator;
-use RecursiveIteratorIterator;
/**
* Class Node
@@ -191,30 +189,6 @@ public function iconList(): array
return $result;
}
- /**
- * Returns a possible classname for the node which can be used as an HTML property.
- *
- * @param string $prefix
- * @return string
- */
- public function getClass(string $prefix = 'ts3_'): string
- {
- if ($this instanceof Channel && $this->isSpacer()) {
- return $prefix.'spacer';
- } elseif ($this instanceof Client && $this['client_type']) {
- return $prefix.'query';
- }
-
- return $prefix.StringHelper::factory(get_class($this))->section('_', 2)->toLower();
- }
-
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- abstract public function getUniqueId(): string;
-
/**
* Returns the name of a possible icon to display the node object.
*
@@ -229,42 +203,6 @@ abstract public function getIcon(): string;
*/
abstract public function getSymbol(): string;
- /**
- * Returns the HTML code to display a TeamSpeak 3 viewer.
- *
- * @param ViewerInterface $viewer
- * @return string
- */
- public function getViewer(ViewerInterface $viewer): string
- {
- // Basic HTML from the root object
- $html = $viewer->fetchObject($this);
-
- // Recursive iterator over all children
- $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
-
- /** @var Node $node */
- foreach ($iterator as $node) {
- $siblings = [];
-
- // Check for each level whether additional elements are present.
- for ($level = 0; $level < $iterator->getDepth(); $level++) {
- $siblings[] = $iterator->getSubIterator($level)->valid() ? 1 : 0;
- }
-
- $siblings[] = ! $iterator->getSubIterator($level)->valid() ? 1 : 0;
-
- $html .= $viewer->fetchObject($node, $siblings);
- }
-
- // Fallback: Empty output
- if (empty($html) && method_exists($viewer, 'toString')) {
- return $viewer->toString();
- }
-
- return $html;
- }
-
/**
* Filters given a node list array using specified filter rules.
*
diff --git a/src/Node/Server.php b/src/Node/Server.php
index 797d8d93..6cb53417 100644
--- a/src/Node/Server.php
+++ b/src/Node/Server.php
@@ -412,8 +412,16 @@ public function channelPermRemove(int $cid, int|array $permid): void
*/
public function channelClientPermList(int $cid, int $cldbid, bool $permsid = false): array
{
- return $this->execute('channelclientpermlist', ['cid' => $cid, 'cldbid' => $cldbid, $permsid ? '-permsid' : null])
- ->toAssocArray($permsid ? 'permsid' : 'permid');
+ try {
+ $result = $this->execute('channelclientpermlist', ['cid' => $cid, 'cldbid' => $cldbid, $permsid ? '-permsid' : null])->toAssocArray($permsid ? 'permsid' : 'permid');
+ } catch (ServerQueryException $e) {
+ if (str_contains($e->getMessage(), 'database empty result set')) {
+ return [];
+ }
+ throw $e;
+ }
+
+ return $result;
}
/**
@@ -742,8 +750,7 @@ public function clientFind(string $pattern): array
*/
public function clientListDb(int $offset = null, int $limit = null): array
{
- return $this->execute('clientdblist -count', ['start' => $offset, 'duration' => $limit])
- ->toAssocArray('cldbid');
+ return $this->execute('clientdblist -count', ['start' => $offset, 'duration' => $limit])->toAssocArray('cldbid');
}
/**
@@ -756,7 +763,21 @@ public function clientListDb(int $offset = null, int $limit = null): array
*/
public function clientCountDb(): int
{
- return current($this->execute('clientdblist -count', ['duration' => 1])->toList());
+ $result = $this->execute('clientdblist -count', ['duration' => 1])->toList();
+
+ if (isset($result[1]['count'])) {
+ return (int) $result[1]['count'];
+ }
+
+ // If the framework returns something different (e.g., flat)
+ if (isset($result['count'])) {
+ return (int) $result['count'];
+ }
+
+ // Fallback – if the result contains only one number
+ $value = current($result);
+
+ return is_numeric($value) ? (int) $value : 0;
}
/**
@@ -770,7 +791,29 @@ public function clientCountDb(): int
*/
public function clientInfoDb(int $cldbid): array
{
- return $this->execute('clientdbinfo', ['cldbid' => $cldbid])->toList();
+ $result = $this->execute('clientdbinfo', ['cldbid' => $cldbid])->toList();
+
+ $metaCldbid = null;
+ if (isset($result[0]['cldbid'])) {
+ $metaCldbid = (int) $result[0]['cldbid'];
+ }
+
+ $filtered = array_values(array_filter($result, static function ($row) {
+ return isset($row['client_database_id']) || isset($row['client_unique_identifier']);
+ }));
+
+ $flat = [];
+ foreach ($filtered as $row) {
+ $flat = array_merge($flat, $row);
+ }
+
+ if (! isset($flat['cldbid']) && $metaCldbid !== null) {
+ $flat['cldbid'] = $metaCldbid;
+ } elseif (! isset($flat['cldbid'])) {
+ $flat['cldbid'] = $cldbid;
+ }
+
+ return $flat;
}
/**
@@ -927,17 +970,26 @@ public function clientGetByDbid(int $dbid): Client
/**
* Returns an array containing the last known nickname and the database ID of the client matching
- * the unique identifier specified with $cluid.
+ * the unique identifier specified with $uid.
*
- * @param string $cluid
+ * @param string $uid
* @return array
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function clientGetNameByUid(string $cluid): array
+ public function clientGetNameByUid(string $uid): array
{
- return $this->execute('clientgetnamefromuid', ['cluid' => $cluid])->toList();
+ $result = [];
+
+ foreach ($this->clientList() as $client) {
+ if ($client['client_unique_identifier'] == $uid) {
+ $result['client_nickname'] = $client['client_nickname'];
+ $result['client_database_id'] = $client['client_database_id'];
+ }
+ }
+
+ return $result;
}
/**
@@ -967,7 +1019,16 @@ public function clientGetIdsByUid(string $cluid): array
*/
public function clientGetNameByDbid(string $cldbid): array
{
- return $this->execute('clientgetnamefromdbid', ['cldbid' => $cldbid])->toList();
+ $result = [];
+
+ foreach ($this->clientList() as $client) {
+ if ($client['client_database_id'] == $cldbid) {
+ $result['client_nickname'] = $client['client_nickname'];
+ $result['client_unique_identifier'] = $client['client_unique_identifier'];
+ }
+ }
+
+ return $result;
}
/**
@@ -1129,10 +1190,16 @@ public function clientSetChannelGroup(int $cldbid, int $cid, int $cgid): void
*/
public function clientPermList(int $cldbid, bool $permsid = false): array
{
- $this->clientListReset();
+ try {
+ $result = $this->execute('clientpermlist', ['cldbid' => $cldbid, $permsid ? '-permsid' : null])->toAssocArray($permsid ? 'permsid' : 'permid');
+ } catch (ServerQueryException $e) {
+ if (str_contains($e->getMessage(), 'database empty result set')) {
+ return [];
+ }
+ throw $e;
+ }
- return $this->execute('clientpermlist', ['cldbid' => $cldbid, $permsid ? '-permsid' : null])
- ->toAssocArray($permsid ? 'permsid' : 'permid');
+ return $result;
}
/**
@@ -1143,20 +1210,27 @@ public function clientPermList(int $cldbid, bool $permsid = false): array
* @param int|int[] $permid
* @param int|int[] $permvalue
* @param bool|bool[] $permskip
+ * @param bool $continueonerror
* @return void
* @throws AdapterException
* @throws ServerQueryException
* @throws TransportException
*/
- public function clientPermAssign(int $cldbid, int|array $permid, int|array $permvalue, bool|array $permskip = false): void
+ public function clientPermAssign(int $cldbid, int|array $permid, int|array $permvalue, bool|array $permskip = false, bool $continueonerror = false): void
{
+ if ($continueonerror) {
+ $continueError = '-continueonerror';
+ } else {
+ $continueError = null;
+ }
+
if (! is_array($permid)) {
$permident = (is_numeric($permid)) ? 'permid' : 'permsid';
} else {
$permident = (is_numeric(current($permid))) ? 'permid' : 'permsid';
}
- $this->execute('clientaddperm -continueonerror', ['cldbid' => $cldbid, $permident => $permid, 'permvalue' => $permvalue, 'permskip' => $permskip]);
+ $this->execute('clientaddperm', [$continueError, 'cldbid' => $cldbid, $permident => $permid, 'permvalue' => $permvalue, 'permskip' => $permskip]);
}
/**
@@ -1281,7 +1355,7 @@ public function serverGroupCopy(int $ssgid, string $name = null, int $tsgid = 0,
for ($i = count($result) - 1; $i >= 0; $i--) {
foreach ($result[$i] as $key => $value) {
if (stripos($key, 'sgid') !== false) {
- // Extrahiere nur die führenden Ziffern
+ // Extract only the leading digits
if (preg_match('/\d+/', $value, $matches)) {
$sgid = (int) $matches[0];
break 2;
@@ -1610,13 +1684,18 @@ public function serverGroupIdentify(
public function channelGroupList(array $filter = []): array
{
if ($this->cgroupList === null) {
- $reply = $this->request('channelgrouplist');
- $raw = $reply->toString(); // StringHelper → String
- $raw = preg_replace('/^.*channelgrouplist/', '', $raw); // Remove prompt & echo
- $raw = trim($raw, "| \n\r\t"); // Remove unnecessary pipes at
- $cgroups = explode('|', $raw);
+ $reply = $this->execute('channelgrouplist');
+ $raw = $reply->toString();
+
+ if (str_contains($raw, 'error id=')) {
+ $raw = substr($raw, 0, strpos($raw, 'error id='));
+ }
+
+ $raw = trim($raw, "| \n\r\t");
+ $cgroups = array_filter(explode('|', $raw));
$this->cgroupList = [];
+
foreach ($cgroups as $line) {
$group = [];
$pairs = explode(' ', $line);
@@ -1624,15 +1703,23 @@ public function channelGroupList(array $filter = []): array
if (! str_contains($pair, '=')) {
continue;
}
+
[$key, $value] = explode('=', $pair, 2);
- $group[$key] = str_replace('\s', ' ', $value); // Replace escapes
+ $value = str_replace('\s', ' ', $value);
+
+ // Automatic typing
+ if (is_numeric($value)) {
+ $value = (int) $value;
+ }
+
+ $group[$key] = $value;
}
if (! isset($group['cgid'])) {
continue;
}
- $cgid = (int) $group['cgid'];
+ $cgid = $group['cgid'];
$this->cgroupList[$cgid] = new ChannelGroup($this, $group);
}
@@ -1706,14 +1793,32 @@ public function channelGroupCopy(int $scgid, string $name = null, int $tcgid = 0
{
$this->channelGroupListReset();
- $cgid = $this->execute('channelgroupcopy', ['scgid' => $scgid, 'tcgid' => $tcgid, 'name' => $name, 'type' => $type])
- ->toList();
+ $result = $this->execute('channelgroupcopy', ['scgid' => $scgid, 'tcgid' => $tcgid, 'name' => $name, 'type' => $type])->toList();
if ($tcgid && $name) {
$this->channelGroupRename($tcgid, $name);
}
- return count($cgid) ? $cgid['cgid'] : $tcgid;
+ // Search for the new scgid in all elements of the result array
+ $cgid = null;
+
+ for ($i = count($result) - 1; $i >= 0; $i--) {
+ foreach ($result[$i] as $key => $value) {
+ if (stripos($key, 'scgid') !== false) {
+ // Extract only the leading digits
+ if (preg_match('/\d+/', $value, $matches)) {
+ $cgid = (int) $matches[0];
+ break 2;
+ }
+ }
+ }
+ }
+
+ if ($cgid === null) {
+ throw new \RuntimeException('channelGroupCopy: Could not determine a valid server group ID.');
+ }
+
+ return $cgid;
}
/**
@@ -1892,9 +1997,18 @@ public function channelGroupClientList(int $cgid = null, int $cid = null, int $c
throw $e;
}
+ // Remove the meta-entry "channelgroupclientlist"
+ $result = array_values(array_filter($result, static function ($row) {
+ // Only keep if these keys exist → real data record
+ return isset($row['cid'], $row['cldbid'], $row['cgid']);
+ }));
+
if ($resolve) {
foreach ($result as $k => $v) {
- $result[$k] = array_merge($v, $this->clientInfoDb($v['cldbid']));
+ $clientInfo = $this->clientInfoDb($v['cldbid']);
+
+ // Now insert the client info block into the actual data record.
+ $result[$k] = array_merge($v, $clientInfo);
}
}
@@ -2689,8 +2803,22 @@ public function tempPasswordDelete(string $pw): void
*/
public function logView(int $lines = 30, int $begin_pos = null, bool $reverse = null, bool $instance = null): array
{
- return $this->execute('logview', ['lines' => $lines, 'begin_pos' => $begin_pos, 'instance' => $instance, 'reverse' => $reverse])
- ->toArray();
+ $result = $this->execute('logview', ['lines' => $lines, 'begin_pos' => $begin_pos, 'instance' => $instance, 'reverse' => $reverse])->toArray();
+
+ // Remove the first meta-entry
+ $filtered = array_filter($result, function ($item) {
+ return is_array($item) && ! array_key_exists('logview', $item) && ! array_key_exists('lines', $item);
+ });
+
+ // Flatten → Only the log lines themselves
+ $flattened = [];
+ foreach ($filtered as $entry) {
+ if (isset($entry['l'])) {
+ $flattened[] = $entry['l'];
+ }
+ }
+
+ return $flattened;
}
/**
@@ -2987,16 +3115,6 @@ public function isOffline(): bool
return $status === 'offline';
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->getParent()->getUniqueId().'_s'.$this->getId();
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Node/ServerGroup.php b/src/Node/ServerGroup.php
index 1ae5f0db..03470aba 100644
--- a/src/Node/ServerGroup.php
+++ b/src/Node/ServerGroup.php
@@ -199,16 +199,6 @@ protected function fetchNodeList(): void
}
}
- /**
- * Returns a unique identifier for the node which can be used as an HTML property.
- *
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->getParent()->getUniqueId().'_sg'.$this->getId();
- }
-
/**
* Returns the name of a possible icon to display the node object.
*
diff --git a/src/Viewer/Html.php b/src/Viewer/Html.php
deleted file mode 100644
index 754028d8..00000000
--- a/src/Viewer/Html.php
+++ /dev/null
@@ -1,657 +0,0 @@
-.
- *
- * @author Sven 'ScP' Paulsen
- * @copyright Copyright (c) Planet TeamSpeak. All rights reserved.
- */
-
-namespace PlanetTeamSpeak\TeamSpeak3Framework\Viewer;
-
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\FileTransferException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\NodeException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Convert;
-use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Channel;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\ChannelGroup;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Client;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Node;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Server;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\ServerGroup;
-use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
-
-/**
- * Class Html
- * @class Html
- * @brief Renders nodes used in HTML-based TeamSpeak 3 viewers.
- */
-class Html implements ViewerInterface
-{
- /**
- * A pre-defined pattern used to display a node in a TeamSpeak 3 viewer.
- *
- * @var string
- */
- protected string $pattern = "\n";
-
- /**
- * The PlanetTeamSpeak\TeamSpeak3Framework\Node\Node object which is currently processed.
- *
- * @var Node|null
- */
- protected null|Node $currObj = null;
-
- /**
- * An array filled with siblings for the PlanetTeamSpeak\TeamSpeak3Framework\Node\Node object which is currently
- * processed.
- *
- * @var array|null
- */
- protected null|array $currSib = null;
-
- /**
- * An internal counter indicating the number of fetched PlanetTeamSpeak\TeamSpeak3Framework\Node\Node objects.
- *
- * @var int
- */
- protected int $currNum = 0;
-
- /**
- * The relative URI path where the images used by the viewer can be found.
- *
- * @var string
- */
- protected string $iconpath;
-
- /**
- * The relative URI path where the country flag icons used by the viewer can be found.
- *
- * @var string|null
- */
- protected null|string $flagpath;
-
- /**
- * The relative path of the file transter client script on the server.
- *
- * @var string|null
- */
- protected null|string $ftclient;
-
- /**
- * Stores an array of local icon IDs.
- *
- * @var array
- */
- protected array $cachedIcons = [100, 200, 300, 400, 500, 600];
-
- /**
- * Stores an array of remote icon IDs.
- *
- * @var array
- */
- protected array $remoteIcons = [];
-
- /**
- * Html constructor.
- *
- * @param string $iconpath
- * @param string|null $flagpath
- * @param string|null $ftclient
- * @param string|null $pattern
- * @return void
- */
- public function __construct(string $iconpath = 'images/viewer/', string $flagpath = null, string $ftclient = null, string $pattern = null)
- {
- $this->iconpath = $iconpath;
- $this->flagpath = $flagpath;
- $this->ftclient = $ftclient;
-
- if ($pattern) {
- $this->pattern = $pattern;
- }
- }
-
- /**
- * Returns the code needed to display a node in a TeamSpeak 3 viewer.
- *
- * @param Node $node
- * @param array $siblings
- * @return string
- * @throws AdapterException
- * @throws FileTransferException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- public function fetchObject(Node $node, array $siblings = []): string
- {
- $this->currObj = $node;
- $this->currSib = $siblings;
-
- $args = [
- $this->getContainerIdent(),
- $this->getContainerClass(),
- $this->getContainerSummary(),
- $this->getRowClass(),
- $this->getPrefixClass(),
- $this->getPrefix(),
- $this->getCorpusClass(),
- $this->getCorpusTitle(),
- $this->getCorpusIcon(),
- $this->getCorpusName(),
- $this->getSuffixClass(),
- $this->getSuffixIcon(),
- $this->getSuffixFlag(),
- ];
-
- return StringHelper::factory($this->pattern)->arg($args);
- }
-
- /**
- * Returns a unique identifier for the current node which can be used as an HTML id
- * property.
- *
- * @return string
- */
- protected function getContainerIdent(): string
- {
- return $this->currObj->getUniqueId();
- }
-
- /**
- * Returns a dynamic string for the current container element which can be used as
- * an HTML class property.
- *
- * @return string
- */
- protected function getContainerClass(): string
- {
- return 'ts3_viewer '.$this->currObj->getClass(null);
- }
-
- /**
- * Returns the ID of the current node which will be used as a summary element for
- * the container element.
- *
- * @return int
- */
- protected function getContainerSummary(): int
- {
- return $this->currObj->getId();
- }
-
- /**
- * Returns a dynamic string for the current row element which can be used as an HTML
- * class property.
- *
- * @return string
- */
- protected function getRowClass(): string
- {
- return ++$this->currNum % 2 ? 'row1' : 'row2';
- }
-
- /**
- * Returns a string for the current prefix element which can be used as an HTML class
- * property.
- *
- * @return string
- */
- protected function getPrefixClass(): string
- {
- return 'prefix '.$this->currObj->getClass(null);
- }
-
- /**
- * Returns the HTML img tags to display the prefix of the current node.
- *
- * @return string
- */
- protected function getPrefix(): string
- {
- $prefix = '';
-
- if (count($this->currSib)) {
- $last = array_pop($this->currSib);
-
- foreach ($this->currSib as $sibling) {
- $prefix .= ($sibling) ? $this->getImage('tree_line.gif') : $this->getImage('tree_blank.png');
- }
-
- $prefix .= ($last) ? $this->getImage('tree_end.gif') : $this->getImage('tree_mid.gif');
- }
-
- return $prefix;
- }
-
- /**
- * Returns a string for the current corpus element which can be used as an HTML class
- * property. If the current node is a channel spacer the class string will contain
- * additional class names to allow further customization of the content via CSS.
- *
- * @return string
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getCorpusClass(): string
- {
- $extras = '';
-
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- switch ($this->currObj->spacerGetType()) {
- case (string) TeamSpeak3::SPACER_SOLIDLINE:
- $extras .= ' solidline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHLINE:
- $extras .= ' dashline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHDOTLINE:
- $extras .= ' dashdotline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHDOTDOTLINE:
- $extras .= ' dashdotdotline';
- break;
-
- case (string) TeamSpeak3::SPACER_DOTLINE:
- $extras .= ' dotline';
- break;
- }
-
- switch ($this->currObj->spacerGetAlign()) {
- case TeamSpeak3::SPACER_ALIGN_CENTER:
- $extras .= ' center';
- break;
-
- case TeamSpeak3::SPACER_ALIGN_RIGHT:
- $extras .= ' right';
- break;
-
- case TeamSpeak3::SPACER_ALIGN_LEFT:
- $extras .= ' left';
- break;
- }
- } elseif ($this->currObj instanceof Client && $this->currObj->client_is_recording) {
- $extras .= ' recording';
- }
-
- return 'corpus '.$this->currObj->getClass(null).$extras;
- }
-
- /**
- * Returns the HTML img tags which can be used to display the various icons for a
- * TeamSpeak_Node_Abstract object.
- *
- * @return string|null
- */
- protected function getCorpusTitle(): null|string
- {
- if ($this->currObj instanceof Server) {
- return 'ID: '.$this->currObj->getId().' | Clients: '.$this->currObj->clientCount().'/'.$this->currObj['virtualserver_maxclients'].' | Uptime: '.Convert::seconds($this->currObj['virtualserver_uptime']);
- } elseif ($this->currObj instanceof Channel && ! $this->currObj->isSpacer()) {
- return 'ID: '.$this->currObj->getId().' | Codec: '.Convert::codec($this->currObj['channel_codec']).' | Quality: '.$this->currObj['channel_codec_quality'];
- } elseif ($this->currObj instanceof Client) {
- return 'ID: '.$this->currObj->getId().' | Version: '.Convert::versionShort($this->currObj['client_version']).' | Platform: '.$this->currObj['client_platform'];
- } elseif ($this->currObj instanceof ServerGroup || $this->currObj instanceof ChannelGroup) {
- return 'ID: '.$this->currObj->getId().' | Type: '.Convert::groupType($this->currObj['type']).' ('.($this->currObj['savedb'] ? 'Permanent' : 'Temporary').')';
- }
-
- return null;
- }
-
- /**
- * Returns an HTML img tag which can be used to display the status icon for a
- * TeamSpeak_Node_Abstract object.
- *
- * @return string
- */
- protected function getCorpusIcon(): string
- {
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- return '';
- }
-
- return $this->getImage($this->currObj->getIcon().'.png');
- }
-
- /**
- * Returns a string for the current corpus element which contains the display name
- * for the current TeamSpeak_Node_Abstract object.
- *
- * @return string
- * @throws AdapterException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getCorpusName(): string
- {
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- if ($this->currObj->spacerGetType() != TeamSpeak3::SPACER_CUSTOM) {
- return '';
- }
-
- $string = $this->currObj['channel_name']->section(']', 1, 99);
-
- if ($this->currObj->spacerGetAlign() == TeamSpeak3::SPACER_ALIGN_REPEAT) {
- $string->resize(30, $string);
- }
-
- return htmlspecialchars($string);
- }
-
- if ($this->currObj instanceof Client) {
- $before = [];
- $behind = [];
-
- if (! $this->currObj->client_is_recording) {
- foreach ($this->currObj->memberOf() as $group) {
- if ($group->getProperty('namemode') == TeamSpeak3::GROUP_NAMEMODE_BEFORE) {
- $before[] = '['.htmlspecialchars($group['name']).']';
- } elseif ($group->getProperty('namemode') == TeamSpeak3::GROUP_NAMEMODE_BEHIND) {
- $behind[] = '['.htmlspecialchars($group['name']).']';
- }
- }
- } else {
- $before[] = '***';
- $behind[] = '*** [RECORDING]';
- }
-
- return implode('', $before).' '.htmlspecialchars($this->currObj).' '.implode('', $behind);
- }
-
- return htmlspecialchars($this->currObj);
- }
-
- /**
- * Returns a string for the current suffix element which can be used as an HTML
- * class property.
- *
- * @return string
- */
- protected function getSuffixClass(): string
- {
- return 'suffix '.$this->currObj->getClass(null);
- }
-
- /**
- * Returns the HTML img tags which can be used to display the various icons for a
- * TeamSpeak_Node_Abstract object.
- *
- * @return string
- * @throws AdapterException
- * @throws FileTransferException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getSuffixIcon(): string
- {
- if ($this->currObj instanceof Server) {
- return $this->getSuffixIconServer();
- } elseif ($this->currObj instanceof Channel) {
- return $this->getSuffixIconChannel();
- } elseif ($this->currObj instanceof Client) {
- return $this->getSuffixIconClient();
- }
-
- return '';
- }
-
- /**
- * Returns the HTML img tags which can be used to display the various icons for a
- * TeamSpeak_Node_Server object.
- *
- * @return string
- * @throws AdapterException
- * @throws FileTransferException
- * @throws ServerQueryException
- * @throws TransportException
- * @throws \Exception
- */
- protected function getSuffixIconServer(): string
- {
- $html = '';
-
- if ($this->currObj['virtualserver_icon_id']) {
- if (! $this->currObj->iconIsLocal('virtualserver_icon_id') && $this->ftclient) {
- if (! isset($this->cacheIcon[$this->currObj['virtualserver_icon_id']])) {
- $download = $this->currObj->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName('virtualserver_icon_id'));
-
- if ($this->ftclient == 'data:image') {
- $download = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port'])->download($download['ftkey'], $download['size']);
- }
-
- $this->cacheIcon[$this->currObj['virtualserver_icon_id']] = $download;
- } else {
- $download = $this->cacheIcon[$this->currObj['virtualserver_icon_id']];
- }
-
- if ($this->ftclient == 'data:image') {
- $html .= $this->getImage('data:'.Convert::imageMimeType($download).';base64,'.base64_encode($download), 'Server Icon', false);
- } else {
- $html .= $this->getImage($this->ftclient.'?ftdata='.base64_encode(serialize($download)), 'Server Icon', false);
- }
- } elseif (in_array($this->currObj['virtualserver_icon_id'], $this->cachedIcons)) {
- $html .= $this->getImage('group_icon_'.$this->currObj['virtualserver_icon_id'].'.png', 'Server Icon');
- }
- }
-
- return $html;
- }
-
- /**
- * Returns the HTML img tags which can be used to display the various icons for a
- * TeamSpeak_Node_Channel object.
- *
- * @return string
- * @throws AdapterException
- * @throws FileTransferException
- * @throws ServerQueryException
- * @throws TransportException
- * @throws \Exception
- */
- protected function getSuffixIconChannel(): string
- {
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- return '';
- }
-
- $html = '';
-
- if ($this->currObj['channel_flag_default']) {
- $html .= $this->getImage('channel_flag_default.png', 'Default Channel');
- }
-
- if ($this->currObj['channel_flag_password']) {
- $html .= $this->getImage('channel_flag_password.png', 'Password-protected');
- }
-
- if ($this->currObj['channel_codec'] == TeamSpeak3::CODEC_CELT_MONO || $this->currObj['channel_codec'] == TeamSpeak3::CODEC_OPUS_MUSIC) {
- $html .= $this->getImage('channel_flag_music.png', 'Music Codec');
- }
-
- if ($this->currObj['channel_needed_talk_power']) {
- $html .= $this->getImage('channel_flag_moderated.png', 'Moderated');
- }
-
- if ($this->currObj['channel_icon_id']) {
- if (! $this->currObj->iconIsLocal('channel_icon_id') && $this->ftclient) {
- if (! isset($this->cacheIcon[$this->currObj['channel_icon_id']])) {
- $download = $this->currObj->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName('channel_icon_id'));
-
- if ($this->ftclient == 'data:image') {
- $download = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port'])->download($download['ftkey'], $download['size']);
- }
-
- $this->cacheIcon[$this->currObj['channel_icon_id']] = $download;
- } else {
- $download = $this->cacheIcon[$this->currObj['channel_icon_id']];
- }
-
- if ($this->ftclient == 'data:image') {
- $html .= $this->getImage('data:'.Convert::imageMimeType($download).';base64,'.base64_encode($download), 'Channel Icon', false);
- } else {
- $html .= $this->getImage($this->ftclient.'?ftdata='.base64_encode(serialize($download)), 'Channel Icon', false);
- }
- } elseif (in_array($this->currObj['channel_icon_id'], $this->cachedIcons)) {
- $html .= $this->getImage('group_icon_'.$this->currObj['channel_icon_id'].'.png', 'Channel Icon');
- }
- }
-
- return $html;
- }
-
- /**
- * Returns the HTML img tags which can be used to display the various icons for a
- * TeamSpeak_Node_Client object.
- *
- * @return string
- * @throws AdapterException
- * @throws FileTransferException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- * @throws \Exception
- */
- protected function getSuffixIconClient(): string
- {
- $html = '';
-
- if ($this->currObj['client_is_priority_speaker']) {
- $html .= $this->getImage('client_priority.png', 'Priority Speaker');
- }
-
- if ($this->currObj['client_is_channel_commander']) {
- $html .= $this->getImage('client_cc.png', 'Channel Commander');
- }
-
- if ($this->currObj['client_is_talker']) {
- $html .= $this->getImage('client_talker.png', 'Talk Power granted');
- } elseif ($cntp = $this->currObj->getParent()->channelGetById($this->currObj['cid'])->channel_needed_talk_power) {
- if ($cntp > $this->currObj['client_talk_power']) {
- $html .= $this->getImage('client_mic_muted.png', 'Insufficient Talk Power');
- }
- }
-
- foreach ($this->currObj->getParent()->memberOf() as $group) {
- if (! $group['iconid']) {
- continue;
- }
-
- $type = ($group instanceof ServerGroup) ? 'Server Group' : 'Channel Group';
-
- if (! $group->iconIsLocal('iconid') && $this->ftclient) {
- if (! isset($this->cacheIcon[$group['iconid']])) {
- $download = $group->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $group->iconGetName('iconid'));
-
- if ($this->ftclient == 'data:image') {
- $download = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port'])->download($download['ftkey'], $download['size']);
- }
-
- $this->cacheIcon[$group['iconid']] = $download;
- } else {
- $download = $this->cacheIcon[$group['iconid']];
- }
-
- if ($this->ftclient == 'data:image') {
- $html .= $this->getImage('data:'.Convert::imageMimeType($download).';base64,'.base64_encode($download), $group.' ['.$type.']', false);
- } else {
- $html .= $this->getImage($this->ftclient.'?ftdata='.base64_encode(serialize($download)), $group.' ['.$type.']', false);
- }
- } elseif (in_array($group['iconid'], $this->cachedIcons)) {
- $html .= $this->getImage('group_icon_'.$group['iconid'].'.png', $group.' ['.$type.']');
- }
- }
-
- if ($this->currObj['client_icon_id']) {
- if (! $this->currObj->iconIsLocal('client_icon_id') && $this->ftclient) {
- if (! isset($this->cacheIcon[$this->currObj['client_icon_id']])) {
- $download = $this->currObj->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName('client_icon_id'));
-
- if ($this->ftclient == 'data:image') {
- $download = TeamSpeak3::factory('filetransfer://'.(str_contains($download['host'], ':') ? '['.$download['host'].']' : $download['host']).':'.$download['port'])->download($download['ftkey'], $download['size']);
- }
-
- $this->cacheIcon[$this->currObj['client_icon_id']] = $download;
- } else {
- $download = $this->cacheIcon[$this->currObj['client_icon_id']];
- }
-
- if ($this->ftclient == 'data:image') {
- $html .= $this->getImage('data:'.Convert::imageMimeType($download).';base64,'.base64_encode($download), 'Client Icon', false);
- } else {
- $html .= $this->getImage($this->ftclient.'?ftdata='.base64_encode(serialize($download)), 'Client Icon', false);
- }
- } elseif (in_array($this->currObj['client_icon_id'], $this->cachedIcons)) {
- $html .= $this->getImage('group_icon_'.$this->currObj['client_icon_id'].'.png', 'Client Icon');
- }
- }
-
- return $html;
- }
-
- /**
- * Returns an HTML img tag which can be used to display the country flag for a
- * TeamSpeak_Node_Client object.
- *
- * @return string
- */
- protected function getSuffixFlag(): string
- {
- if (! $this->currObj instanceof Client) {
- return '';
- }
-
- if ($this->flagpath && $this->currObj['client_country']) {
- return $this->getImage($this->currObj['client_country']->toLower().'.png', $this->currObj['client_country'], false, true);
- }
-
- return '';
- }
-
- /**
- * Returns the code to display a custom HTML img tag.
- *
- * @param string $name
- * @param string $text
- * @param bool $iconpath
- * @param bool $flagpath
- * @return string
- */
- protected function getImage(string $name, string $text = '', bool $iconpath = true, bool $flagpath = false): string
- {
- $src = '';
-
- if ($iconpath) {
- $src = $this->iconpath;
- }
-
- if ($flagpath) {
- $src = $this->flagpath;
- }
-
- return "
";
- }
-}
diff --git a/src/Viewer/Json.php b/src/Viewer/Json.php
deleted file mode 100644
index 5db94ee6..00000000
--- a/src/Viewer/Json.php
+++ /dev/null
@@ -1,475 +0,0 @@
-.
- *
- * @author Sven 'ScP' Paulsen
- * @copyright Copyright (c) Planet TeamSpeak. All rights reserved.
- */
-
-namespace PlanetTeamSpeak\TeamSpeak3Framework\Viewer;
-
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\NodeException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
-use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Convert;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Channel;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\ChannelGroup;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Client;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Node;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Server;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\ServerGroup;
-use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
-use stdClass;
-
-/**
- * Class Json
- * @class PlanetTeamSpeak\TeamSpeak3Framework\Viewer\Json
- * @brief Generates a JSON struct used in JS-based TeamSpeak 3 viewers.
- */
-class Json implements ViewerInterface
-{
- /**
- * Stores an array of data parsed from PlanetTeamSpeak\TeamSpeak3Framework\Node\Node objects.
- *
- * @var array
- */
- protected array $data;
-
- /**
- * The PlanetTeamSpeak\TeamSpeak3Framework\Node\Node object which is currently processed.
- *
- * @var Node|null
- */
- protected ?Node $currObj = null;
-
- /**
- * An array filled with siblings for the PlanetTeamSpeak\TeamSpeak3Framework\Node\Node object which is currently
- * processed.
- *
- * @var array|null
- */
- protected ?array $currSib = null;
-
- /**
- * An internal counter indicating the depth of the PlanetTeamSpeak\TeamSpeak3Framework\Node\Node object previously
- * processed.
- *
- * @var int
- */
- protected int $lastLvl = 0;
-
- protected int $id = 0;
-
- protected int $icon = 0;
-
- /**
- * The Json constructor.
- *
- * @param array $data
- * @return Json
- */
- public function __construct(array &$data = [])
- {
- $this->data = &$data;
-
- return $this;
- }
-
- /**
- * Assembles an stdClass object for the current element.
- *
- * @param Node $node
- * @param array $siblings
- * @return string
- * @throws AdapterException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- public function fetchObject(Node $node, array $siblings = []): string
- {
- $this->currObj = $node;
- $this->currSib = $siblings;
-
- $obj = new stdClass();
-
- $obj->ident = $this->getId();
- $obj->parent = $this->getParent();
- $obj->children = $node->count();
- $obj->level = $this->getLevel();
- $obj->first = $obj->level != $this->lastLvl;
- $obj->last = (bool) array_pop($siblings);
- $obj->siblings = array_map('boolval', $siblings);
- $obj->class = $this->getType();
- $obj->name = $this->getName();
- $obj->image = $this->getImage();
- $obj->props = $this->getProps();
-
- $this->data[] = $obj;
- $this->lastLvl = $obj->level;
-
- return '';
- }
-
- /**
- * Returns the ID of the current element.
- *
- * @return false|string
- */
- protected function getId(): bool|string
- {
- if ($this->currObj instanceof Server) {
- return 'ts3_s'.$this->currObj->virtualserver_id;
- } elseif ($this->currObj instanceof Channel) {
- return 'ts3_c'.$this->currObj->cid;
- } elseif ($this->currObj instanceof Client) {
- return 'ts3_u'.$this->currObj->clid;
- }
-
- return false;
- }
-
- /**
- * Returns the parent ID of the current element.
- *
- * @return string
- */
- protected function getParent(): string
- {
- if ($this->currObj instanceof Channel) {
- return $this->currObj->pid ? 'ts3_c'.$this->currObj->pid : 'ts3_s'.$this->currObj->getParent()->getId();
- } elseif ($this->currObj instanceof Client) {
- return $this->currObj->cid ? 'ts3_c'.$this->currObj->cid : 'ts3_s'.$this->currObj->getParent()->getId();
- }
-
- return 'ts3';
- }
-
- /**
- * Returns the level of the current element.
- *
- * @return int
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getLevel(): int
- {
- if ($this->currObj instanceof Channel) {
- return $this->currObj->getLevel() + 2;
- } elseif ($this->currObj instanceof Client) {
- return $this->currObj->getParent()->channelGetById($this->currObj->cid)->getLevel() + 3;
- }
-
- return 1;
- }
-
- /**
- * Returns a single type identifier for the current element.
- *
- * @return string
- */
- protected function getType(): string
- {
- if ($this->currObj instanceof Server) {
- return 'server';
- } elseif ($this->currObj instanceof Channel) {
- return 'channel';
- } elseif ($this->currObj instanceof Client) {
- return 'client';
- } elseif ($this->currObj instanceof ServerGroup || $this->currObj instanceof ChannelGroup) {
- return 'group';
- }
-
- return 'host';
- }
-
- /**
- * Returns a string for the current corpus element which can be used as an HTML class
- * property. If the current node is a channel spacer the class string will contain
- * additional class names to allow further customization of the content via CSS.
- *
- * @return string
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getClass(): string
- {
- $extras = '';
-
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- switch ($this->currObj->spacerGetType()) {
- case (string) TeamSpeak3::SPACER_SOLIDLINE:
- $extras .= ' solidline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHLINE:
- $extras .= ' dashline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHDOTLINE:
- $extras .= ' dashdotline';
- break;
-
- case (string) TeamSpeak3::SPACER_DASHDOTDOTLINE:
- $extras .= ' dashdotdotline';
- break;
-
- case (string) TeamSpeak3::SPACER_DOTLINE:
- $extras .= ' dotline';
- break;
- }
-
- switch ($this->currObj->spacerGetAlign()) {
- case TeamSpeak3::SPACER_ALIGN_REPEAT:
- $extras .= ' repeat';
- break;
-
- case TeamSpeak3::SPACER_ALIGN_CENTER:
- $extras .= ' center';
- break;
-
- case TeamSpeak3::SPACER_ALIGN_RIGHT:
- $extras .= ' right';
- break;
-
- case TeamSpeak3::SPACER_ALIGN_LEFT:
- $extras .= ' left';
- break;
- }
- }
-
- return $this->currObj->getClass(null).$extras;
- }
-
- /**
- * Returns an individual type for a spacer.
- *
- * @return string
- * @throws AdapterException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getSpacerType(): string
- {
- $type = '';
-
- if (! $this->currObj instanceof Channel || ! $this->currObj->isSpacer()) {
- return 'none';
- }
-
- $type .= match ($this->currObj->spacerGetType()) {
- (string) TeamSpeak3::SPACER_SOLIDLINE => 'solidline',
- (string) TeamSpeak3::SPACER_DASHLINE => 'dashline',
- (string) TeamSpeak3::SPACER_DASHDOTLINE => 'dashdotline',
- (string) TeamSpeak3::SPACER_DASHDOTDOTLINE => 'dashdotdotline',
- (string) TeamSpeak3::SPACER_DOTLINE => 'dotline',
- default => 'custom',
- };
-
- if ($type == 'custom') {
- $type .= match ($this->currObj->spacerGetAlign()) {
- TeamSpeak3::SPACER_ALIGN_REPEAT => 'repeat',
- TeamSpeak3::SPACER_ALIGN_CENTER => 'center',
- TeamSpeak3::SPACER_ALIGN_RIGHT => 'right',
- default => 'left',
- };
- }
-
- return $type;
- }
-
- /**
- * Returns a string for the current corpus element which contains the display name
- * for the current TeamSpeak_Node_Abstract object.
- *
- * @return string
- * @throws AdapterException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getName(): string
- {
- if ($this->currObj instanceof Channel && $this->currObj->isSpacer()) {
- return $this->currObj['channel_name']->section(']', 1, 99)->toString();
- } elseif ($this->currObj instanceof Client) {
- $before = [];
- $behind = [];
-
- foreach ($this->currObj->memberOf() as $group) {
- if ($group->getProperty('namemode') == TeamSpeak3::GROUP_NAMEMODE_BEFORE) {
- $before[] = '['.$group['name'].']';
- } elseif ($group->getProperty('namemode') == TeamSpeak3::GROUP_NAMEMODE_BEHIND) {
- $behind[] = '['.$group['name'].']';
- }
- }
-
- return trim(implode('', $before).' '.$this->currObj.' '.implode('', $behind));
- }
-
- return $this->currObj->toString();
- }
-
- /**
- * Returns the parent ID of the current element.
- *
- * @return stdClass
- * @throws AdapterException
- * @throws NodeException
- * @throws ServerQueryException
- * @throws TransportException
- */
- protected function getProps(): stdClass
- {
- $props = new stdClass();
-
- if (is_a($this->currObj, Node::class)) {
- $this->id = 0;
- $this->icon = 0;
- $props->version = $this->currObj->getParent()->getAdapter()->getHost()->version('version')->toString();
- $props->platform = $this->currObj->getParent()->getAdapter()->getHost()->version('platform')->toString();
- $props->users = $this->currObj->virtualservers_total_clients_online;
- $props->slots = $this->currObj->virtualservers_total_maxclients;
- $props->flags = 0;
- } elseif (is_a($this->currObj, Server::class)) {
- $props->id = $this->currObj->getId();
- $props->icon = ($this->currObj->virtualserver_icon_id < 0) ? pow(2, 32) - ($this->currObj->virtualserver_icon_id * -1) : $this->currObj->virtualserver_icon_id;
- $props->welcmsg = strlen($this->currObj->virtualserver_welcomemessage) ? trim($this->currObj->virtualserver_welcomemessage) : null;
- $props->hostmsg = strlen($this->currObj->virtualserver_hostmessage) ? trim($this->currObj->virtualserver_hostmessage) : null;
- $props->version = Convert::versionShort($this->currObj->virtualserver_version)->toString();
- $props->platform = $this->currObj->virtualserver_platform->toString();
- $props->country = null;
- $props->users = $this->currObj->clientCount();
- $props->slots = $this->currObj->virtualserver_maxclients;
- $props->flags = 0;
-
- $props->flags += $this->currObj->virtualserver_status === 'online' ? 1 : 0;
- $props->flags += $this->currObj->virtualserver_flag_password ? 2 : 0;
- $props->flags += $this->currObj->virtualserver_autostart ? 4 : 0;
- $props->flags += $this->currObj->virtualserver_weblist_enabled ? 8 : 0;
- $props->flags += $this->currObj->virtualserver_ask_for_privilegekey ? 16 : 0;
- } elseif (is_a($this->currObj, Channel::class)) {
- $props->id = $this->currObj->getId();
- $props->icon = 0;
- if (! $this->currObj->isSpacer()) {
- $props->icon = $this->currObj->channel_icon_id < 0 ? (2 ** 32) - ($this->currObj->channel_icon_id * -1) : $this->currObj->channel_icon_id;
- }
-
- $props->path = trim($this->currObj->getPathway());
- $props->topic = strlen($this->currObj->channel_topic) ? trim($this->currObj->channel_topic) : null;
- $props->codec = $this->currObj->channel_codec;
- $props->users = $this->currObj->total_clients == -1 ? 0 : $this->currObj->total_clients;
- $props->slots = $this->currObj->channel_maxclients == -1 ? $this->currObj->getParent()->virtualserver_maxclients : $this->currObj->channel_maxclients;
- $props->famusers = $this->currObj->total_clients_family == -1 ? 0 : $this->currObj->total_clients_family;
- $props->famslots = $this->currObj->channel_maxfamilyclients == -1 ? $this->currObj->getParent()->virtualserver_maxclients : $this->currObj->channel_maxfamilyclients;
- $props->spacer = $this->getSpacerType();
- $props->flags = 0;
-
- $props->flags += $this->currObj->channel_flag_default ? 1 : 0;
- $props->flags += $this->currObj->channel_flag_password ? 2 : 0;
- $props->flags += $this->currObj->channel_flag_permanent ? 4 : 0;
- $props->flags += $this->currObj->channel_flag_semi_permanent ? 8 : 0;
- $props->flags += ($props->codec == 3 || $props->codec == 5) ? 16 : 0;
- $props->flags += $this->currObj->channel_needed_talk_power != 0 ? 32 : 0;
- $props->flags += $this->currObj->total_clients != -1 ? 64 : 0;
- $props->flags += $this->currObj->isSpacer() ? 128 : 0;
- } elseif (is_a($this->currObj, Client::class)) {
- $props->id = $this->currObj->getId();
- $props->icon = $this->currObj->client_icon_id < 0 ? pow(2, 32) - ($this->currObj->client_icon_id * -1) : $this->currObj->client_icon_id;
- $props->version = Convert::versionShort($this->currObj->client_version)->toString();
- $props->platform = $this->currObj->client_platform->toString();
- $props->country = strlen($this->currObj->client_country) ? trim($this->currObj->client_country) : null;
- $props->awaymesg = strlen($this->currObj->client_away_message) ? trim($this->currObj->client_away_message) : null;
- $props->memberof = [];
- $props->badges = $this->currObj->getBadges();
- $props->flags = 0;
-
- foreach ($this->currObj->memberOf() as $num => $group) {
- $props->memberof[$num] = new stdClass();
-
- $props->memberof[$num]->name = trim($group->name);
- $props->memberof[$num]->icon = $group->iconid < 0 ? pow(2, 32) - ($group->iconid * -1) : $group->iconid;
- $props->memberof[$num]->order = $group->sortid;
- $props->memberof[$num]->flags = 0;
-
- $props->memberof[$num]->flags += $group->namemode;
- $props->memberof[$num]->flags += $group->type == 2 ? 4 : 0;
- $props->memberof[$num]->flags += $group->type == 0 ? 8 : 0;
- $props->memberof[$num]->flags += $group->savedb ? 16 : 0;
- $props->memberof[$num]->flags += $group instanceof ServerGroup ? 32 : 0;
- }
-
- $props->flags += $this->currObj->client_away ? 1 : 0;
- $props->flags += $this->currObj->client_is_recording ? 2 : 0;
- $props->flags += $this->currObj->client_is_channel_commander ? 4 : 0;
- $props->flags += $this->currObj->client_is_priority_speaker ? 8 : 0;
- $props->flags += $this->currObj->client_is_talker ? 16 : 0;
- $props->flags += $this->currObj->getParent()->channelGetById($this->currObj->cid)->channel_needed_talk_power > $this->currObj->client_talk_power && ! $this->currObj->client_is_talker ? 32 : 0;
- $props->flags += $this->currObj->client_input_muted || ! $this->currObj->client_input_hardware ? 64 : 0;
- $props->flags += $this->currObj->client_output_muted || ! $this->currObj->client_output_hardware ? 128 : 0;
- } elseif (is_a($this->currObj, ServerGroup::class) || is_a($this->currObj, ChannelGroup::class)) {
- $props->id = $this->currObj->getId();
- $props->icon = $this->currObj->iconid < 0 ? pow(2, 32) - ($this->currObj->iconid * -1) : $this->currObj->iconid;
- $props->order = $this->currObj->sortid;
- $props->n_map = $this->currObj->n_member_addp;
- $props->n_mrp = $this->currObj->n_member_removep;
- $props->flags = 0;
-
- $props->flags += $this->currObj->namemode;
- $props->flags += $this->currObj->type == 2 ? 4 : 0;
- $props->flags += $this->currObj->type == 0 ? 8 : 0;
- $props->flags += $this->currObj->savedb ? 16 : 0;
- $props->flags += $this->currObj instanceof ServerGroup ? 32 : 0;
- }
-
- return $props;
- }
-
- /**
- * Returns the status icon URL of the current element.
- *
- * @return string
- */
- protected function getImage(): string
- {
- return str_replace('_', '-', $this->currObj->getIcon());
- }
-
- /**
- * Returns a string representation of this node.
- *
- * @return string
- */
- public function toString(): string
- {
- return $this->__toString();
- }
-
- /**
- * Returns a string representation of this node.
- *
- * @return string
- */
- public function __toString()
- {
- return json_encode($this->data);
- }
-}
diff --git a/src/Viewer/Text.php b/src/Viewer/Text.php
deleted file mode 100644
index ec78e4bf..00000000
--- a/src/Viewer/Text.php
+++ /dev/null
@@ -1,110 +0,0 @@
-.
- *
- * @author Sven 'ScP' Paulsen
- * @copyright Copyright (c) Planet TeamSpeak. All rights reserved.
- */
-
-namespace PlanetTeamSpeak\TeamSpeak3Framework\Viewer;
-
-use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Node;
-
-/**
- * @class Text
- * @brief Renders nodes used in ASCII-based TeamSpeak 3 viewers.
- */
-class Text implements ViewerInterface
-{
- /**
- * A pre-defined pattern used to display a node in a TeamSpeak 3 viewer.
- *
- * @var string
- */
- protected string $pattern = "%0%1 %2\n";
-
- protected null|Node $currObj = null;
-
- protected null|array $currSib = null;
-
- /**
- * Returns the code needed to display a node in a TeamSpeak 3 viewer.
- *
- * @param Node $node
- * @param array $siblings
- * @return string
- */
- public function fetchObject(Node $node, array $siblings = []): string
- {
- $this->currObj = $node;
- $this->currSib = $siblings;
-
- $args = [
- $this->getPrefix(),
- $this->getCorpusIcon(),
- $this->getCorpusName(),
- ];
-
- return StringHelper::factory($this->pattern)->arg($args);
- }
-
- /**
- * Returns the ASCII string to display the prefix of the current node.
- *
- * @return string
- */
- protected function getPrefix(): string
- {
- $prefix = '';
-
- if (count($this->currSib)) {
- $last = array_pop($this->currSib);
-
- foreach ($this->currSib as $sibling) {
- $prefix .= ($sibling) ? '| ' : ' ';
- }
-
- $prefix .= ($last) ? '\\-' : '|-';
- }
-
- return $prefix;
- }
-
- /**
- * Returns an ASCII string which can be used to display the status icon for a
- * TeamSpeak_Node_Abstract object.
- *
- * @return string
- */
- protected function getCorpusIcon(): string
- {
- return $this->currObj->getSymbol();
- }
-
- /**
- * Returns a string for the current corpus element which contains the display name
- * for the current Node object.
- *
- * @return string
- */
- protected function getCorpusName(): string
- {
- return $this->currObj;
- }
-}
diff --git a/src/Viewer/ViewerInterface.php b/src/Viewer/ViewerInterface.php
deleted file mode 100644
index 95a45d57..00000000
--- a/src/Viewer/ViewerInterface.php
+++ /dev/null
@@ -1,42 +0,0 @@
-.
- *
- * @author Sven 'ScP' Paulsen
- * @copyright Copyright (c) Planet TeamSpeak. All rights reserved.
- */
-
-namespace PlanetTeamSpeak\TeamSpeak3Framework\Viewer;
-
-use PlanetTeamSpeak\TeamSpeak3Framework\Node\Node;
-
-/**
- * @class PlanetTeamSpeak\TeamSpeak3Framework\Viewer\ViewerInterface
- * @brief Interface class describing a TeamSpeak 3 viewer.
- */
-interface ViewerInterface
-{
- /**
- * Returns the code needed to display a node in a TeamSpeak 3 viewer.
- *
- * @param Node $node
- * @param array $siblings
- * @return string
- */
- public function fetchObject(Node $node, array $siblings = []): string;
-}
diff --git a/tests/DevLiveServer/ChannelGroupTest.php b/tests/DevLiveServer/ChannelGroupTest.php
new file mode 100644
index 00000000..6f920e51
--- /dev/null
+++ b/tests/DevLiveServer/ChannelGroupTest.php
@@ -0,0 +1,280 @@
+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->ts3_unit_test_channel_name = str_replace('DEV_LIVE_SERVER_UNIT_TEST_CHANNEL=', '', preg_replace('#\n(?!\n)#', '', $env[7]));
+ $this->ts3_unit_test_userName = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER=', '', preg_replace('#\n(?!\n)#', '', $env[9]));
+ } else {
+ $this->active = 'false';
+ }
+
+ $this->ts3_server_uri = 'serverquery://'.$this->user.':'.$this->password.'@'.$this->host.':'.$this->queryPort.
+ '/?server_port=9987'.
+ '&no_query_clients=0'.
+ '&timeout=30';
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_get_channelgroup_by_name()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $this->set_play_test_channelgroup($this->ts3_VirtualServer);
+
+ $channelgroup = $this->ts3_VirtualServer->channelGroupGetByName('UnitTest');
+
+ $this->assertIsString($channelgroup['name']);
+ $this->assertEquals('UnitTest', $channelgroup['name']);
+
+ $this->unset_play_test_channelgroup($this->ts3_VirtualServer);
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_rename_channelgroup()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $this->set_play_test_channelgroup($this->ts3_VirtualServer);
+
+ $channelgroup = $this->ts3_VirtualServer->channelGroupGetByName('UnitTest');
+ $channelgroup->rename('UnitTest-Renamed');
+ $renamedChannelGroup = $this->ts3_VirtualServer->channelGroupGetByName('UnitTest-Renamed');
+
+ $this->assertIsString($renamedChannelGroup['name']);
+ $this->assertEquals('UnitTest-Renamed', $renamedChannelGroup['name']);
+
+ $this->unset_play_test_channelgroup($this->ts3_VirtualServer);
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_copy_delete_channelgroup()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $this->set_play_test_channelgroup($this->ts3_VirtualServer);
+
+ $this->ts3_VirtualServer->channelGroupGetByName('UnitTest')->copy('UnitTest-Copy');
+ $copiedChannelGroup = $this->ts3_VirtualServer->channelGroupGetByName('UnitTest-Copy');
+
+ $this->assertIsString($copiedChannelGroup['name']);
+ $this->assertEquals('UnitTest-Copy', $copiedChannelGroup['name']);
+ $this->ts3_VirtualServer->channelGroupGetByName('UnitTest-Copy')->delete();
+
+ try {
+ $this->ts3_VirtualServer->channelGroupGetByName('UnitTest-Copy');
+ $this->fail('ServerGroup should not exist');
+ } catch (ServerQueryException $e) {
+ $this->assertEquals('invalid groupID', $e->getMessage());
+ }
+
+ $this->unset_play_test_channelgroup($this->ts3_VirtualServer);
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws HelperException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ */
+ public function test_can_assign_remove_permissions_to_channelgroup()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $this->set_play_test_channelgroup($this->ts3_VirtualServer);
+
+ $this->ts3_VirtualServer->channelGroupPermAssign($this->cgid, ['i_client_private_textmessage_power'], [75]);
+ $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->permAssign(['i_client_talk_power'], 75);
+
+ $permList = $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->permList(true);
+ $this->assertEquals(75, $permList['i_client_talk_power']['permvalue']);
+ $this->assertEquals(75, $permList['i_client_private_textmessage_power']['permvalue']);
+
+ $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->permRemove(['i_client_private_textmessage_power']);
+ $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->permRemove(['i_client_talk_power']);
+
+ $permListKeyRemoved = $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->permList(true);
+
+ $this->assertArrayNotHasKey('i_client_talk_power', $permListKeyRemoved);
+ $this->assertArrayNotHasKey('i_client_private_textmessage_power', $permListKeyRemoved);
+
+ $this->unset_play_test_channelgroup($this->ts3_VirtualServer);
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws HelperException
+ */
+ public function test_channelGroupList()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $channelgrouplist = $this->ts3_VirtualServer->channelGroupList(['type' => 1]);
+
+ foreach ($channelgrouplist as $channelgroup) {
+ $this->assertContains($channelgroup['name'], ['Channel Admin', 'Guest', 'Operator']);
+ $this->assertIsInt($channelgroup['cgid']);
+ }
+
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws HelperException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ */
+ public function test_channelGroupClientList()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ //prepare
+ $cid = $this->ts3_VirtualServer->channelGetByName($this->ts3_unit_test_channel_name)->getId();
+ $this->set_play_test_channelgroup($this->ts3_VirtualServer);
+
+ $createdCID = $this->ts3_VirtualServer->channelCreate(['channel_name' => 'Play-Test', 'channel_flag_permanent' => 1, 'cpid' => $cid]);
+ $this->ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($createdCID);
+ $this->ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->setChannelGroup($createdCID, $this->cgid);
+
+ $channelGroupClientList = $this->ts3_VirtualServer->channelGroupGetById($this->cgid)->clientList(null, null, true);
+
+ foreach ($channelGroupClientList as $client) {
+ $this->assertEquals($this->ts3_unit_test_userName, $client['client_nickname']);
+ }
+
+ $this->ts3_VirtualServer->channeldelete($createdCID, true);
+ $this->unset_play_test_channelgroup($this->ts3_VirtualServer);
+ $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws ServerQueryException
+ * @throws TransportException
+ */
+ private function set_play_test_channelgroup(Server $ts3VirtualServer): void
+ {
+ $this->cgid = $ts3VirtualServer->channelGroupCreate('UnitTest', 1);
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws ServerQueryException
+ * @throws TransportException
+ */
+ public function unset_play_test_channelgroup(Server $ts3_VirtualServer): void
+ {
+ $ts3_VirtualServer->channelGroupDelete($this->cgid, true);
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ */
+ public function dev_reset_channelgroup(): void
+ {
+ $channelgrouplist = $this->ts3_VirtualServer->channelGroupList(['type' => 1]);
+ foreach ($channelgrouplist as $channelgroup) {
+ if ($channelgroup['name'] != 'Channel Admin' && $channelgroup['name'] != 'Guest' && $channelgroup['name'] != 'Operator') {
+ $this->ts3_VirtualServer->channelGroupDelete($channelgroup['cgid'], true);
+ }
+ }
+ }
+}
diff --git a/tests/DevLiveServer/ChannelTest.php b/tests/DevLiveServer/ChannelTest.php
index bdaa89e0..cbf5e0a2 100644
--- a/tests/DevLiveServer/ChannelTest.php
+++ b/tests/DevLiveServer/ChannelTest.php
@@ -395,15 +395,22 @@ public function test_can_set_channel_permissions()
$this->set_play_test_channel($ts3_VirtualServer);
$testCid = $ts3_VirtualServer->channelCreate(['channel_name' => 'Standard Channel', 'channel_flag_permanent' => 1, 'cpid' => $this->test_cid]);
- $ts3_VirtualServer->channelPermAssign($testCid, ['i_channel_needed_join_power'], [50]);
- $ts3_VirtualServer->channelPermAssign($testCid, ['i_channel_needed_subscribe_power'], [50]);
+ $ts3_VirtualServer->channelGetById($testCid)->permAssign(['i_channel_needed_join_power'], [50]);
+ $ts3_VirtualServer->channelGetById($testCid)->permAssign(['i_channel_needed_subscribe_power'], [50]);
- $channel = $ts3_VirtualServer->channelGetById($testCid);
- $channelPermission = $channel->permList(true);
+ $channelPermission = $ts3_VirtualServer->channelGetById($testCid)->permList(true);
$this->assertEquals(50, $channelPermission['i_channel_needed_join_power']['permvalue']);
$this->assertEquals(50, $channelPermission['i_channel_needed_subscribe_power']['permvalue']);
+ $ts3_VirtualServer->channelGetById($testCid)->permRemove(['i_channel_needed_join_power']);
+ $ts3_VirtualServer->channelGetById($testCid)->permRemove(['i_channel_needed_subscribe_power']);
+
+ $channelPermissionRemoved = $ts3_VirtualServer->channelGetById($testCid)->permList(true);
+
+ $this->assertArrayNotHasKey('i_channel_needed_join_power', $channelPermissionRemoved);
+ $this->assertArrayNotHasKey('i_channel_needed_subscribe_power', $channelPermissionRemoved);
+
$this->unset_play_test_channel($ts3_VirtualServer);
$ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
}
diff --git a/tests/DevLiveServer/ClientTest.php b/tests/DevLiveServer/ClientTest.php
index cd963e85..ec87724f 100644
--- a/tests/DevLiveServer/ClientTest.php
+++ b/tests/DevLiveServer/ClientTest.php
@@ -37,8 +37,12 @@ class ClientTest extends TestCase
private string $ts3_unit_test_userName;
+ private string $ts3_unit_test_userName2 = '';
+
private int $test_cid;
+ private int $cgid;
+
public function setUp(): void
{
//proof test active
@@ -53,6 +57,7 @@ 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->ts3_unit_test_userName2 = str_replace('DEV_LIVE_SERVER_UNIT_TEST_USER_EXTEND=', '', preg_replace('#\n(?!\n)#', '', $env[11]));
} else {
$this->active = 'false';
}
@@ -85,6 +90,9 @@ public function test_can_get_user_attributes()
$this->assertIsArray($userInfo);
$this->assertEquals($this->ts3_unit_test_userName, $userInfo['client_nickname']);
+ $symbol = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->getSymbol();
+ $this->assertEquals('@', $symbol);
+
$this->unset_play_test_channel($ts3_VirtualServer);
$ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
}
@@ -142,7 +150,7 @@ public function test_can_move_user()
$testCid = $ts3_VirtualServer->channelCreate(['channel_name' => 'Standard Channel', 'channel_flag_permanent' => 1, 'cpid' => $this->test_cid]);
$userID = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->getId();
- $ts3_VirtualServer->clientMove($userID, $testCid);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($testCid);
$userMoved = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->getInfo();
$this->assertEquals($userMoved['cid'], $testCid);
@@ -171,7 +179,7 @@ public function test_can_move_user()
* @throws AdapterException
* @throws HelperException
*/
- public function test_can_send_client_text_message()
+ public function test_can_send_client_group_text_message()
{
if ($this->user_test_active == 'false' || $this->active == 'false') {
$this->markTestSkipped('DevLiveServer ist not active');
@@ -181,11 +189,29 @@ public function test_can_send_client_text_message()
$userID = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->getId();
$this->assertIsInt($userID);
- $userID = $ts3_VirtualServer->clientGetById($userID);
- $this->assertIsObject($userID);
- $userID->message('Hello World');
+ $ts3_VirtualServer->clientGetById($userID)->message('Hello World');
+
+ if (! empty($this->ts3_unit_test_userName2)) {
+ $this->dev_reset_channelgroup($ts3_VirtualServer);
+ //send a message via a group
+ $this->set_play_test_channelgroup($ts3_VirtualServer);
+ $this->set_play_test_channel($ts3_VirtualServer);
+
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($this->test_cid);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName2)->move($this->test_cid);
+
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->setChannelGroup($this->test_cid, $this->cgid);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName2)->setChannelGroup($this->test_cid, $this->cgid);
+
+ $ts3_VirtualServer->channelgroupGetById($this->cgid)->message('UnitTestToGroup');
+
+ $symbol = $ts3_VirtualServer->channelgroupGetById($this->cgid)->getSymbol();
+ $this->assertEquals('%', $symbol);
+
+ $this->unset_play_test_channel($ts3_VirtualServer);
+ $this->unset_play_test_channelgroup($ts3_VirtualServer);
+ }
- $this->asserttrue(true);
$ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
}
@@ -228,7 +254,9 @@ public function test_can_find_client_by_name_pattern()
$userFindings = $ts3_VirtualServer->clientFind('UnitT');
foreach ($userFindings as $user) {
- $this->assertEquals($this->ts3_unit_test_userName, $user['client_nickname']);
+ if ($user['client_nickname'] == 'UnitTestUser') {
+ $this->assertEquals($this->ts3_unit_test_userName, $user['client_nickname']);
+ }
}
$this->asserttrue(true);
@@ -272,15 +300,25 @@ public function test_can_get_clientInfoDB()
$ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
$clientListDb = $ts3_VirtualServer->clientListDb();
+ $dbCount = $ts3_VirtualServer->clientCountDb();
+ $this->assertGreaterThanOrEqual(1, $dbCount);
foreach ($clientListDb as $client) {
if ($client['client_nickname'] == $this->ts3_unit_test_userName) {
- $clientInfoDB = $ts3_VirtualServer->clientInfoDb($client['cldbid']);
+ $clientInfoDB = $ts3_VirtualServer->clientGetByName($client['client_nickname'])->infoDb();
}
}
$this->assertIsArray($clientInfoDB);
+ $ts3_VirtualServer->clientGetByDbid($clientInfoDB['client_database_id'])->modifyDb(['client_description'=> 'unittest']);
+ $result = $ts3_VirtualServer->clientGetByDbid($clientInfoDB['client_database_id'])->infoDb();
+ $this->assertEquals('unittest', $result['client_description']);
+
+ $ts3_VirtualServer->clientGetByDbid($clientInfoDB['client_database_id'])->modifyDb(['client_description'=> '']);
+ $result2 = $ts3_VirtualServer->clientGetByDbid($clientInfoDB['client_database_id'])->infoDb();
+ $this->assertEquals('', $result2['client_description']);
+
$this->asserttrue(true);
$ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
}
@@ -359,6 +397,10 @@ public function test_can_add_list_del_client_to_servergroup()
$ts3_VirtualServer->serverGroupGetById($sgid)->clientDel($clidDB['client_database_id']);
+ //test over Client.php
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->addServerGroup($sgid);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->remServerGroup($sgid);
+
//remember at this point the test will fail if the user is still in the servergroup
// unset will not force delete the user from the servergroup
$ts3_VirtualServer->serverGroupDelete($sgid);
@@ -366,6 +408,263 @@ public function test_can_add_list_del_client_to_servergroup()
$this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
}
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_get_by_clientGetByDbid()
+ {
+ if ($this->active == 'false' || $this->user_test_active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ $user = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName);
+
+ $cliByDBid = $ts3_VirtualServer->clientGetByDbid($user['client_database_id']);
+ $this->assertIsString($cliByDBid['client_nickname']);
+ $this->assertEquals($this->ts3_unit_test_userName, $cliByDBid['client_nickname']);
+
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_clientGetNameByUid()
+ {
+ if ($this->active == 'false' || $this->user_test_active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ $user = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName);
+ $result = $ts3_VirtualServer->clientGetNameByUid($user['client_unique_identifier']);
+
+ $this->assertIsArray($result);
+ $this->assertEquals($this->ts3_unit_test_userName, $result['client_nickname']);
+ $this->assertEquals($user['client_database_id'], $result['client_database_id']);
+
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_clientGetNameByDbid()
+ {
+ if ($this->active == 'false' || $this->user_test_active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ $user = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName);
+ $result = $ts3_VirtualServer->clientGetNameByDbid($user['client_database_id']);
+
+ $this->assertIsArray($result);
+ $this->assertEquals($this->ts3_unit_test_userName, $result['client_nickname']);
+ $this->assertEquals($user['client_unique_identifier'], $result['client_unique_identifier']);
+
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_handle_permission()
+ {
+ if ($this->active == 'false' || $this->user_test_active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $permList = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permList(true);
+
+ //expect the client itself has no permissions
+ $this->assertIsArray($permList);
+ $this->assertEmpty($permList);
+
+ //now add permission at the client level
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permAssign(['i_client_poke_power'], 75);
+ $result = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permList(true);
+
+ $this->assertIsArray($result);
+ $this->assertNotEmpty($result);
+
+ foreach ($result as $perm) {
+ $this->assertEquals('i_client_poke_power', $perm['permsid']);
+ $this->assertEquals(75, $perm['permvalue']);
+ }
+
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permAssign(['i_client_poke_power'], 40);
+ $result2 = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permList(true);
+
+ $this->assertIsArray($result2);
+ $this->assertNotEmpty($result2);
+
+ foreach ($result2 as $perm) {
+ $this->assertEquals('i_client_poke_power', $perm['permsid']);
+ $this->assertEquals(40, $perm['permvalue']);
+ }
+
+ //remove permission
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permRemove(['i_client_poke_power']);
+ $result3 = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permList(true);
+ $this->assertIsArray($result3);
+ $this->assertEmpty($result3);
+
+ $this->set_play_test_channel($ts3_VirtualServer);
+
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($this->test_cid);
+ $permChannelOverView = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->permOverview($this->test_cid);
+ $this->assertIsArray($permChannelOverView);
+
+ $this->unset_play_test_channel($ts3_VirtualServer);
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws HelperException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ */
+ public function test_can_handle_permission_chain_channel()
+ {
+ if ($this->active == 'false' || $this->user_test_active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ //prepare
+ $cid = $ts3_VirtualServer->channelGetByName($this->ts3_unit_test_channel_name)->getId();
+ $createdCID = $ts3_VirtualServer->channelCreate(['channel_name' => 'Play-Test', 'channel_flag_permanent' => 1, 'cpid' => $cid]);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($createdCID);
+
+ $clientList = $ts3_VirtualServer->channelGetById($createdCID)->clientList();
+
+ foreach ($clientList as $client) {
+ if ($client['client_nickname'] == $this->ts3_unit_test_userName) {
+ $cldbid = $client['client_database_id'];
+ $channelPermList = $ts3_VirtualServer->channelGetById($createdCID)->clientPermList($client['client_database_id'], true);
+ }
+ }
+
+ //expect the client itself has no permissions
+ $this->assertIsArray($channelPermList);
+ $this->assertEmpty($channelPermList);
+
+ //now add permission at the client-channel level
+ $ts3_VirtualServer->channelGetById($createdCID)->clientPermAssign($cldbid, ['i_client_poke_power'], 75);
+ $result = $ts3_VirtualServer->channelGetById($createdCID)->clientPermList($cldbid, true);
+
+ $this->assertIsArray($result);
+ $this->assertNotEmpty($result);
+
+ foreach ($result as $perm) {
+ $this->assertEquals('i_client_poke_power', $perm['permsid']);
+ $this->assertEquals(75, $perm['permvalue']);
+ }
+
+ $ts3_VirtualServer->channelGetById($createdCID)->clientPermAssign($cldbid, ['i_client_poke_power'], 40);
+ $result2 = $ts3_VirtualServer->channelGetById($createdCID)->clientPermList($cldbid, true);
+
+ $this->assertIsArray($result2);
+ $this->assertNotEmpty($result2);
+
+ foreach ($result2 as $perm) {
+ $this->assertEquals('i_client_poke_power', $perm['permsid']);
+ $this->assertEquals(40, $perm['permvalue']);
+ }
+
+ //remove permission
+ $ts3_VirtualServer->channelGetById($createdCID)->clientPermRemove($cldbid, ['i_client_poke_power']);
+ $result3 = $ts3_VirtualServer->channelGetById($createdCID)->clientPermList($cldbid, true);
+ $this->assertIsArray($result3);
+ $this->assertEmpty($result3);
+
+ $ts3_VirtualServer->channelGetById($createdCID)->delete(true);
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws \Exception
+ */
+ public function test_channelGroupClientList()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+ $this->set_play_test_channel($ts3_VirtualServer);
+
+ // Resetting lists
+ $ts3_VirtualServer->clientListReset();
+ $ts3_VirtualServer->channelGroupListReset();
+
+ // Get servergroup client info
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->move($this->test_cid);
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->setChannelGroup($this->test_cid, 6);
+ $channelGroupList = $ts3_VirtualServer->channelGroupClientList(null, null, null, true);
+
+ $channelgroup_clientlist = [];
+ foreach ($channelGroupList as $channelgroup) {
+ $channelgroup_clientlist[$channelgroup['cgid']] = count($ts3_VirtualServer->channelGroupClientList($channelgroup['cgid']));
+ }
+
+ $this->assertIsArray($channelgroup_clientlist);
+ $this->assertGreaterThan(0, $channelgroup_clientlist[6]);
+
+ $this->unset_play_test_channel($ts3_VirtualServer);
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws HelperException
+ */
+ public function test_has_overwolf()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
+
+ $result = $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName)->hasOverwolf();
+ $this->assertFalse($result);
+
+ $ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
+ $this->assertFalse($ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
+ }
+
/**
* @throws AdapterException
* @throws ServerQueryException
@@ -388,7 +687,11 @@ public function test_can_ban_user()
}
if (isset($userID)) {
- $ts3_VirtualServer->clientBan($userID, 600, 'Unittest');
+ $ts3_VirtualServer->clientGetById($userID)->ban(600, 'Unittest');
+ }
+
+ if ($this->ts3_unit_test_userName2 !== '') {
+ $ts3_VirtualServer->clientGetByName($this->ts3_unit_test_userName2)->kick(TeamSpeak3::KICK_SERVER, 'Unittest');
}
$banlist = $ts3_VirtualServer->banList();
@@ -428,4 +731,39 @@ public function unset_play_test_channel($ts3_VirtualServer): void
{
$ts3_VirtualServer->channelDelete($this->test_cid, true);
}
+
+ /**
+ * @throws AdapterException
+ * @throws ServerQueryException
+ * @throws TransportException
+ */
+ private function set_play_test_channelgroup(Server $ts3VirtualServer): void
+ {
+ $this->cgid = $ts3VirtualServer->channelGroupCreate('UnitTest', 1);
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws ServerQueryException
+ * @throws TransportException
+ */
+ public function unset_play_test_channelgroup(Server $ts3_VirtualServer): void
+ {
+ $ts3_VirtualServer->channelGroupDelete($this->cgid, true);
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ */
+ public function dev_reset_channelgroup(Server $ts3_VirtualServer): void
+ {
+ $channelgrouplist = $ts3_VirtualServer->channelGroupList(['type' => 1]);
+ foreach ($channelgrouplist as $channelgroup) {
+ if ($channelgroup['name'] != 'Channel Admin' && $channelgroup['name'] != 'Guest' && $channelgroup['name'] != 'Operator') {
+ $ts3_VirtualServer->channelGroupDelete($channelgroup['cgid'], true);
+ }
+ }
+ }
}
diff --git a/tests/DevLiveServer/ConnectionTest.php b/tests/DevLiveServer/ConnectionTest.php
index 644c0187..3cae0eb3 100644
--- a/tests/DevLiveServer/ConnectionTest.php
+++ b/tests/DevLiveServer/ConnectionTest.php
@@ -4,6 +4,7 @@
use PHPUnit\Framework\TestCase;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException;
+use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
@@ -129,4 +130,149 @@ public function test_can_ssh_multiple_connect_with_different_nicknames()
$this->assertEquals('UnitTestBot2', $whoami2['client_nickname']);
$this->assertEquals('UnitTestBot3', $whoami3['client_nickname']);
}
+
+ /**
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws AdapterException
+ * @throws HelperException
+ */
+ public function test_can_get_host_information()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_host = TeamSpeak3::factory($this->ts3_server_uri);
+ $port = $ts3_host->getParent()->serverSelectedPort();
+ $this->assertEquals(9987, $port);
+
+ $version = $ts3_host->version();
+ $this->assertIsArray($version);
+ $this->assertArrayHasKey('version', $version);
+ $this->assertArrayHasKey('platform', $version);
+ $this->assertEquals('Linux', $version['platform']);
+ $this->assertArrayHasKey('build', $version);
+
+ $serverID = $ts3_host->serverIdGetByPort(9987);
+ $this->assertIsInt($serverID);
+ $this->assertEquals(1, $serverID);
+
+ $PortByID = $ts3_host->serverGetPortById($serverID);
+ $this->assertIsInt($PortByID);
+ $this->assertEquals(9987, $PortByID);
+
+ $server = $ts3_host->servergetByname('UnitTestServer');
+ $this->assertIsArray($server);
+ $this->assertArrayHasKey('virtualserver_name', $server);
+ $this->assertArrayHasKey('virtualserver_uptime', $server);
+
+ $serverByUID = $ts3_host->serverGetByUid($server['virtualserver_unique_identifier']);
+ $this->assertArrayHasKey('virtualserver_name', $serverByUID);
+ $this->assertArrayHasKey('virtualserver_uptime', $serverByUID);
+
+ $permList = $ts3_host->permissionList();
+ $this->assertIsArray($permList);
+ $this->assertArrayHasKey('b_serverinstance_help_view', $permList);
+ $this->assertArrayHasKey('permid', $permList['b_serverinstance_help_view']);
+ $this->assertArrayHasKey('permname', $permList['b_serverinstance_help_view']);
+ $this->assertArrayHasKey('permcatid', $permList['b_serverinstance_help_view']);
+
+ $permCats = $ts3_host->permissionCats();
+ $this->assertIsArray($permCats);
+ $this->assertArrayHasKey('PERM_CAT_GLOBAL', $permCats);
+ $this->assertArrayHasKey('PERM_CAT_GROUP_DELETE', $permCats);
+ $this->assertArrayHasKey('PERM_CAT_CLIENT_BASICS', $permCats);
+
+ $permTree = $ts3_host->permissionTree();
+ $this->assertIsArray($permTree);
+ $this->assertArrayHasKey('permcatid', $permTree[16]);
+ $this->assertArrayHasKey('permcatname', $permTree[16]);
+ $this->assertEquals('Global', $permTree[16]['permcatname']);
+
+ $permFind = $ts3_host->permissionFind(['b_virtualserver_info_view']);
+ $this->assertIsArray($permFind[0]);
+ $this->assertArrayHasKey('t', $permFind[0]);
+ $this->assertArrayHasKey('id1', $permFind[0]);
+ $this->assertArrayHasKey('id2', $permFind[0]);
+
+ $permFindMultiple = $ts3_host->permissionFind(['b_virtualserver_info_view', 'b_virtualserver_channel_list']);
+ $this->assertIsArray($permFindMultiple[0]);
+ $this->assertArrayHasKey('t', $permFindMultiple[0]);
+ $this->assertArrayHasKey('id1', $permFindMultiple[0]);
+ $this->assertArrayHasKey('id2', $permFindMultiple[0]);
+ $this->assertArrayHasKey('t', $permFindMultiple[1]);
+ $this->assertArrayHasKey('id1', $permFindMultiple[1]);
+ $this->assertArrayHasKey('id2', $permFindMultiple[1]);
+
+ try {
+ $ts3_host->permissionFind(['b_serverinstance_help_view']);
+ } catch (ServerQueryException $e) {
+ $this->assertEquals('invalid permission ID', $e->getMessage());
+ }
+
+ $permID = $ts3_host->permissionGetIdByName('b_virtualserver_info_view');
+ $this->assertIsInt($permID);
+
+ $permName = $ts3_host->permissionGetNameById($permID);
+ $this->assertEquals('b_virtualserver_info_view', $permName);
+
+ $selfPermCheck = $ts3_host->selfPermCheck(['b_virtualserver_info_view']);
+ $this->assertIsArray($selfPermCheck);
+ $this->assertArrayHasKey('permsid', $selfPermCheck);
+ $this->assertIsString($selfPermCheck['permsid']);
+ $this->assertEquals('b_virtualserver_info_view', $selfPermCheck['permsid']);
+ $this->assertEquals(1, $selfPermCheck['permvalue']);
+
+ $ts3_host->getAdapter()->getTransport()->disconnect();
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws HelperException
+ */
+ public function test_can_handle_log()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_host = TeamSpeak3::factory($this->ts3_server_uri);
+ $ts3_host->serverGetByPort(9987)->logAdd('UnitTest', TeamSpeak3::LOGLEVEL_DEBUG);
+ $log = $ts3_host->serverGetByPort(9987)->logView();
+ $this->assertIsArray($log);
+ $this->assertIsString($log[29]);
+ $this->assertStringContainsString('UnitTest', $log[29]);
+
+ $ts3_host->getAdapter()->getTransport()->disconnect();
+ }
+
+ /**
+ * @throws AdapterException
+ * @throws TransportException
+ * @throws ServerQueryException
+ * @throws HelperException
+ */
+ public function test_can_handle_server_query()
+ {
+ if ($this->active == 'false') {
+ $this->markTestSkipped('DevLiveServer ist not active');
+ }
+
+ $ts3_host = TeamSpeak3::factory($this->ts3_server_uri);
+ $countQuery = $ts3_host->queryCountLogin();
+ $this->assertIsInt($countQuery);
+ $this->assertEquals(1, $countQuery);
+
+ $queryLoginlist = $ts3_host->queryListLogin();
+
+ foreach ($queryLoginlist as $queryLogin) {
+ $this->assertIsString($queryLogin['client_login_name']);
+ $this->assertEquals('ts3-bot-dev', $queryLogin['client_login_name']);
+ }
+
+ $ts3_host->getAdapter()->getTransport()->disconnect();
+ }
}
diff --git a/tests/DevLiveServer/RefactorFunctionsTest.php b/tests/DevLiveServer/RefactorFunctionsTest.php
index 724bbd42..123a8e63 100644
--- a/tests/DevLiveServer/RefactorFunctionsTest.php
+++ b/tests/DevLiveServer/RefactorFunctionsTest.php
@@ -117,37 +117,6 @@ public function test_serverGroupList_serverGroupClientList()
$this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
}
- /**
- * @throws TransportException
- * @throws ServerQueryException
- * @throws AdapterException
- * @throws \Exception
- */
- public function test_channelGroupList_channelGroupClientList()
- {
- if ($this->active == 'false') {
- $this->markTestSkipped('DevLiveServer ist not active');
- }
-
- $this->ts3_VirtualServer = TeamSpeak3::factory($this->ts3_server_uri);
-
- // Resetting lists
- $this->ts3_VirtualServer->clientListReset();
- $this->ts3_VirtualServer->channelGroupListReset();
-
- // Get servergroup client info
- $this->ts3_VirtualServer->clientList(['client_type' => 0]);
- $channelGroupList = $this->ts3_VirtualServer->channelGroupList();
-
- $channelgroup_clientlist = [];
- foreach ($channelGroupList as $channelgroup) {
- $channelgroup_clientlist[$channelgroup->cgid] = count($this->ts3_VirtualServer->channelGroupClientList($channelgroup->cgid));
- }
-
- $this->ts3_VirtualServer->getAdapter()->getTransport()->disconnect();
- $this->assertFalse($this->ts3_VirtualServer->getAdapter()->getTransport()->isConnected());
- }
-
/**
* @throws AdapterException
* @throws TransportException
diff --git a/tests/DevLiveServer/ServerGroupTest.php b/tests/DevLiveServer/ServerGroupTest.php
index 5c8f3e2b..e0a01cd1 100644
--- a/tests/DevLiveServer/ServerGroupTest.php
+++ b/tests/DevLiveServer/ServerGroupTest.php
@@ -123,7 +123,7 @@ public function test_can_rename_servergroup()
* @throws NodeException
* @throws HelperException
*/
- public function test_can_copy_servergroup()
+ public function test_can_copy_delete_servergroup()
{
if ($this->active == 'false') {
$this->markTestSkipped('DevLiveServer ist not active');
@@ -136,7 +136,7 @@ public function test_can_copy_servergroup()
$getDuplicatedServerGroup = $this->ts3_VirtualServer->serverGroupGetById($duplicatedSGID);
$this->assertEquals('UnitTest-Copy', $getDuplicatedServerGroup['name']);
- $this->ts3_VirtualServer->serverGroupDelete($duplicatedSGID);
+ $this->ts3_VirtualServer->serverGroupGetById($duplicatedSGID)->delete();
try {
$this->ts3_VirtualServer->serverGroupGetById($duplicatedSGID);
$this->fail('ServerGroup should not exist');
@@ -237,11 +237,12 @@ private function set_play_test_servergroup(Server $ts3VirtualServer): void
}
/**
+ * @param Server $ts3_VirtualServer
* @throws AdapterException
* @throws ServerQueryException
- * @throws HelperException
+ * @throws TransportException
*/
- public function unset_play_test_servergroup($ts3_VirtualServer): void
+ public function unset_play_test_servergroup(Server $ts3_VirtualServer): void
{
$ts3_VirtualServer->serverGroupDelete($this->sgid);
}
diff --git a/tests/Helper/CharTest.php b/tests/Helper/CharTest.php
index d426d574..c8822680 100644
--- a/tests/Helper/CharTest.php
+++ b/tests/Helper/CharTest.php
@@ -120,6 +120,29 @@ public function testASCIISpace()
$this->assertIsInt($char->toInt());
}
+ public function testFromHexFailed()
+ {
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage("given parameter 'A' is not a valid hexadecimal number");
+
+ Char::fromHex('A'); // odd length
+
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage("given parameter 'GG' is not a valid hexadecimal number");
+
+ Char::fromHex('GG'); // no valid hex characters
+
+ // hex2bin() returns false if the number of characters is odd.
+ // This allows us to trigger the second throw path specifically.
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage("given parameter 'F' could not be converted to binary data");
+
+ // // To bypass the first if block and let hex2bin() fail itself,
+ Char::fromHex('F');
+ //!!!Attention!!!
+ //Throw at // Hex → Binary string (UTF-8 compatible) is not reachable. The first if block caught the issue
+ }
+
/**
* @throws HelperException
*/
@@ -256,10 +279,98 @@ public function testUnicode1Byte()
static::calculateUTF8Ordinal("\x7F"),
Char::fromHex('7F')->toUnicode()
);
+
+ //
+ // 1-BYTE UTF-8 (U+0000 – U+007F)
+ //
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\x00"),
+ Char::fromHex('00')->toUnicode()
+ );
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\x7F"),
+ Char::fromHex('7F')->toUnicode()
+ );
+
+ //
+ // INVALID LEADING BYTE (< 0xC2)
+ // e.g., 0x80 – 0xC1 should return false
+ //
+ $this->assertEquals(-1, Char::fromHex('80')->toUnicode());
+ $this->assertEquals(-1, Char::fromHex('C1')->toUnicode());
+
+ //
+ // 2-BYTE UTF-8 (U+0080 – U+07FF)
+ // Example: '¢' (U+00A2) → C2 A2
+ //
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xC2\xA2"),
+ Char::fromHex('C2A2')->toUnicode()
+ );
+
+ // Upper end of 2-byte range: '߿' (U+07FF) → DF BF
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xDF\xBF"),
+ Char::fromHex('DFBF')->toUnicode()
+ );
+
+ //
+ // 3-BYTE UTF-8 (U+0800 – U+FFFF)
+ // Example: '€' (U+20AC) → E2 82 AC
+ //
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xE2\x82\xAC"),
+ Char::fromHex('E282AC')->toUnicode()
+ );
+
+ // Upper end of 3-byte range: '' (U+FFFF) → EF BF BF
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xEF\xBF\xBF"),
+ Char::fromHex('EFBFBF')->toUnicode()
+ );
+
+ //
+ // 4-BYTE UTF-8 (U+10000 – U+10FFFF)
+ // Example: '😀' (U+1F600) → F0 9F 98 80
+ //
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xF0\x9F\x98\x80"),
+ Char::fromHex('F09F9880')->toUnicode()
+ );
+
+ // Upper end: U+10FFFF → F4 8F BF BF
+ $this->assertEquals(
+ static::calculateUTF8Ordinal("\xF4\x8F\xBF\xBF"),
+ Char::fromHex('F48FBFBF')->toUnicode()
+ );
+
+ //
+ // INVALID TOO-HIGH LEAD BYTE (> 0xF4)
+ //
+ $this->assertEquals(
+ -1,
+ Char::fromHex('F5')->toUnicode()
+ );
+ }
+
+ public function testUnicodeFailed()
+ {
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage('char parameter may not contain more or less than one UTF-8 character');
+
+ new Char('');
+
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage('char parameter may not contain more or less than one UTF-8 character');
+
+ new Char('ab'); // 2 chars
+
+ $this->expectException(HelperException::class);
+ new Char('😀😀'); // 2 UTF-8 Codepoints
}
/**
- * Return integer value of a string, specifically for UTF8 strings.
+ * Return an integer value of a string, specifically for UTF8 strings.
*
* @param string $char
*
@@ -267,14 +378,30 @@ public function testUnicode1Byte()
*/
private static function calculateUTF8Ordinal(string $char): int
{
- $charString = mb_substr($char, 0, 1, 'utf-8');
- $charLength = strlen($charString);
- $ordinal = ord($charString[0]) & (0xFF >> $charLength);
- //Merge other characters into the value
- for ($i = 1; $i < $charLength; $i++) {
- $ordinal = $ordinal << 6 | (ord($charString[$i]) & 127);
+ $bytes = array_map('ord', str_split($char));
+ $length = strlen($char);
+
+ if ($length === 1) {
+ // 1-byte (ASCII)
+ return $bytes[0];
+ } elseif ($length === 2) {
+ // 2-byte
+ return (($bytes[0] & 0x1F) << 6) |
+ ($bytes[1] & 0x3F);
+ } elseif ($length === 3) {
+ // 3-byte
+ return (($bytes[0] & 0x0F) << 12) |
+ (($bytes[1] & 0x3F) << 6) |
+ ($bytes[2] & 0x3F);
+ } elseif ($length === 4) {
+ // 4-byte
+ return (($bytes[0] & 0x07) << 18) |
+ (($bytes[1] & 0x3F) << 12) |
+ (($bytes[2] & 0x3F) << 6) |
+ ($bytes[3] & 0x3F);
}
- return $ordinal;
+ // invalid UTF-8 (longer than 4 bytes)
+ return -1;
}
}
diff --git a/tests/Helper/ConvertTest.php b/tests/Helper/ConvertTest.php
index 403e5bc1..30fd12e9 100644
--- a/tests/Helper/ConvertTest.php
+++ b/tests/Helper/ConvertTest.php
@@ -4,6 +4,8 @@
use PHPUnit\Framework\TestCase;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Convert;
+use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
+use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
class ConvertTest extends TestCase
{
@@ -213,6 +215,9 @@ public function testConvertSecondsToHumanReadable()
$output = Convert::seconds(-90.083);
$this->assertEquals('-0D 00:01:30', $output);
$this->assertIsString($output);
+
+ $result = Convert::seconds(5000, true);
+ $this->assertEquals('0D 00:00:05', $result);
}
public function testConvertCodecIDToHumanReadable()
@@ -365,7 +370,6 @@ public function testConvertLogLevelIDToHumanReadable()
public function testConvertLogEntryToArray()
{
- // @todo: Implement matching integration test for testing real log entries
$mock_data = [
'2017-06-26 21:55:30.307009|INFO |Query | |query from 47 [::1]:62592 issued: login with account "serveradmin"(serveradmin)',
];
@@ -377,6 +381,30 @@ public function testConvertLogEntryToArray()
'Log entry appears malformed, dumping: '.print_r($entryParsed, true)
);
}
+
+ $entry = '2024-01-01 12:00:00|ERROR MESSAGE';
+
+ $result = Convert::logEntry($entry);
+
+ $this->assertEquals(0, $result['timestamp']);
+ $this->assertEquals(TeamSpeak3::LOGLEVEL_ERROR, $result['level']);
+ $this->assertEquals('ParamParser', $result['channel']);
+ $this->assertEquals('', $result['server_id']);
+ $this->assertTrue($result['malformed']);
+ $this->assertEquals($entry, $result['msg_plain']);
+
+ // msg is a StringHelper object
+ $this->assertInstanceOf(StringHelper::class, $result['msg']);
+ $this->assertStringContainsString('convert error', (string) $result['msg']);
+
+ $entry = '2024-01-01 12:00:00|INFO|system|1|All good';
+ $result = Convert::logEntry($entry);
+
+ $this->assertFalse($result['malformed']);
+ $this->assertIsInt($result['timestamp']);
+ $this->assertEquals('system', $result['channel']);
+ $this->assertEquals('1', $result['server_id']);
+ $this->assertEquals('All good', (string) $result['msg']);
}
public function testConvertToPassword()
@@ -412,5 +440,34 @@ public function testDetectImageMimeType()
base64_decode('R0lGODdhAQABAIAAAPxqbAAAACwAAAAAAQABAAACAkQBADs=')
)
);
+
+ //fake binary
+ $fakeBinary = 'NOT_AN_IMAGE';
+ $result = Convert::imageMimeType($fakeBinary);
+ $this->assertEquals('image/svg+xml', $result);
+ }
+
+ public function testIconIdUnsignedBelowThreshold()
+ {
+ // Sample value below the 0x80000000 bit (small ID)
+ $value = 123456789;
+ $result = Convert::iconId($value);
+
+ $this->assertEquals($value, $result);
+ }
+
+ public function testIconIdUnsignedWithHighBitSet()
+ {
+ // Set the 0x80000000 bit → should be interpreted as negative
+ $value = 0x80000001; // 2147483649
+ $result = Convert::iconId($value);
+
+ if (PHP_INT_SIZE > 4) {
+ // 2147483649 - 0x100000000 = -2147483647
+ $this->assertEquals(-2147483647, $result);
+ } else {
+ // No overflow handling on 32-bit systems
+ $this->assertEquals($value, $result);
+ }
}
}
diff --git a/tests/Helper/StringTest.php b/tests/Helper/StringTest.php
index 2eb6d9b4..6b297e1a 100644
--- a/tests/Helper/StringTest.php
+++ b/tests/Helper/StringTest.php
@@ -5,6 +5,7 @@
use Exception;
use PHPUnit\Framework\TestCase;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException;
+use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Char;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;
@@ -628,4 +629,358 @@ public function testJsonSerialize()
json_encode(['a' => 'Hello world!'])
);
}
+
+ public function testResizeTruncatesWhenTooLong()
+ {
+ $str = new StringHelper('abcdef');
+ $resized = $str->resize(3);
+
+ $this->assertEquals('abc', (string) $resized);
+ }
+
+ public function testResizePadsWhenTooShort()
+ {
+ $str = new StringHelper('abc');
+ $resized = $str->resize(5, '_');
+
+ $this->assertEquals('abc__', (string) $resized);
+ }
+
+ public function testResizeUnchangedWhenSameSize()
+ {
+ $str = new StringHelper('abcd');
+ $resized = $str->resize(4, 'x');
+
+ $this->assertEquals('abcd', (string) $resized);
+ }
+
+ public function testFilterAlnumRemovesNonAlnumCharacters()
+ {
+ $str = new StringHelper('abc-123!@#xyz');
+ $result = $str->filterAlnum();
+
+ // Removes all special characters, leaving only a–z, A–Z, 0–9
+ $this->assertEquals('abc123xyz', (string) $result);
+ }
+
+ public function testFilterAlnumKeepsAlnumOnly()
+ {
+ $str = new StringHelper('A1b2C3');
+ $result = $str->filterAlnum();
+
+ $this->assertEquals('A1b2C3', (string) $result);
+ }
+
+ public function testFilterAlnumOnEmptyString()
+ {
+ $str = new StringHelper('');
+ $result = $str->filterAlnum();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testFilterAlphaRemovesNonLetters()
+ {
+ $str = new StringHelper('abc123!@#XYZ');
+ $result = $str->filterAlpha();
+
+ // Nur Buchstaben bleiben
+ $this->assertEquals('abcXYZ', (string) $result);
+ }
+
+ public function testFilterAlphaKeepsLettersOnly()
+ {
+ $str = new StringHelper('AbCdEf');
+ $result = $str->filterAlpha();
+
+ $this->assertEquals('AbCdEf', (string) $result);
+ }
+
+ public function testFilterAlphaRemovesAllIfNoLetters()
+ {
+ $str = new StringHelper('12345_?!-');
+ $result = $str->filterAlpha();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testFilterAlphaEmptyString()
+ {
+ $str = new StringHelper('');
+ $result = $str->filterAlpha();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testUriSafeBasicConversion()
+ {
+ $str = new StringHelper('Hello World!');
+ $result = $str->uriSafe();
+
+ $this->assertEquals('hello-world', (string) $result);
+ }
+
+ public function testUriSafeWithCustomSpacer()
+ {
+ $str = new StringHelper('Hello World!');
+ $result = $str->uriSafe('_');
+
+ $this->assertEquals('hello_world', (string) $result);
+ }
+
+ public function testUriSafeTrimsExtraSpacers()
+ {
+ $str = new StringHelper('hello---world');
+ $result = $str->uriSafe();
+
+ $this->assertEquals('hello-world', (string) $result);
+ }
+
+ public function testUriSafeWithOnlySpecialCharacters()
+ {
+ $str = new StringHelper('@@@');
+ $result = $str->uriSafe();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testUriSafeReturnsNewInstance()
+ {
+ $str = new StringHelper('Test');
+ $result = $str->uriSafe();
+
+ // Should NOT be the same object (since ‘new self’)
+ $this->assertNotSame($str, $result);
+ }
+
+ /**
+ * Test StringHelper "magic" __call TODO we should change this in the future
+ * @return void
+ */
+ public function testCallThrowsOnUndefinedFunction()
+ {
+ $str = new StringHelper('test');
+
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessage("cannot call undefined function 'nope'");
+
+ $str->nope();
+ }
+
+ /**
+ * Test StringHelper "magic" __call TODO we should change this in the future
+ * @return void
+ */
+ public function testCallWithArgsAndSelfReplacement()
+ {
+ $str = new StringHelper('hello world');
+ $result = $str->str_replace('world', 'there', $str);
+
+ $this->assertInstanceOf(StringHelper::class, $result);
+ $this->assertEquals('hello there', (string) $result);
+ }
+
+ /**
+ * Test StringHelper "magic" __call TODO we should change this in the future
+ * @return void
+ */
+ public function testCallThrowsWhenMissingObjectParameter()
+ {
+ $str = new StringHelper('abc');
+
+ $this->expectException(HelperException::class);
+ $this->expectExceptionMessageMatches('/without the .* object parameter/');
+
+ // Keine Referenz auf $this in Argumenten → Exception
+ $str->str_replace('a', 'b');
+ }
+
+ /**
+ * Test StringHelper "magic" __call TODO we should change this in the future
+ * @return void
+ */
+ public function testCallWithoutArgsReturnsModifiedString()
+ {
+ $str = new StringHelper('hello');
+ $result = $str->strtoupper();
+
+ $this->assertEquals('HELLO', (string) $result);
+ }
+
+ /**
+ * Test StringHelper "magic" __call TODO we should change this in the future
+ * @return void
+ */
+ public function testCallReturnsNonStringValue()
+ {
+ $str = new StringHelper('abcdef');
+ $len = $str->strlen();
+
+ $this->assertIsInt($len);
+ $this->assertEquals(6, $len);
+ }
+
+ public function testKeyReturnsCurrentPosition()
+ {
+ $str = new StringHelper('abc');
+
+ // If the class implements Iterator, position could initially be 0.
+ $this->assertEquals(0, $str->key());
+ $this->assertIsInt($str->key());
+
+ // Optional: if there is a method such as next() or rewind(),
+ // you can check that the position has changed.
+ if (method_exists($str, 'next')) {
+ $str->next();
+ $this->assertEquals(1, $str->key());
+ }
+ }
+
+ public function testOffsetExistsWithinBounds()
+ {
+ $str = new StringHelper('abc');
+
+ // Index 0, 1, 2 exist (since strlen = 3)
+ $this->assertTrue($str->offsetExists(0));
+ $this->assertTrue($str->offsetExists(2));
+ }
+
+ public function testOffsetExistsOutOfBounds()
+ {
+ $str = new StringHelper('abc');
+
+ // Index 3 ist außerhalb des zulässigen Bereichs (0–2 gültig)
+ $this->assertFalse($str->offsetExists(3));
+ $this->assertFalse($str->offsetExists(99));
+ }
+
+ public function testOffsetGetReturnsCharWhenOffsetExists()
+ {
+ $str = new StringHelper('abc');
+ $char = $str->offsetGet(1); // 'b'
+
+ $this->assertInstanceOf(Char::class, $char);
+ $this->assertEquals('b', (string) $char);
+ }
+
+ public function testOffsetGetReturnsNullWhenOffsetDoesNotExist()
+ {
+ $str = new StringHelper('abc');
+ $result = $str->offsetGet(10); // outside the length
+
+ $this->assertNull($result);
+ }
+
+ public function testOffsetSetReplacesCharacterWhenOffsetExists()
+ {
+ $str = new StringHelper('abc');
+ $str->offsetSet(1, 'Z'); // replaces ‘b’ with 'Z'
+
+ $this->assertEquals('aZc', (string) $str);
+ }
+
+ public function testOffsetSetDoesNothingWhenOffsetDoesNotExist()
+ {
+ $str = new StringHelper('abc');
+ $str->offsetSet(10, 'Z'); // invalid index, no change
+
+ $this->assertEquals('abc', (string) $str);
+ }
+
+ public function testOffsetUnsetRemovesCharacterWhenOffsetExists()
+ {
+ $str = new StringHelper('abcd');
+ $str->offsetUnset(1); // removes 'b'
+
+ $this->assertEquals('acd', (string) $str);
+ }
+
+ public function testOffsetUnsetDoesNothingWhenOffsetDoesNotExist()
+ {
+ $str = new StringHelper('abcd');
+ $str->offsetUnset(10); // invalid index, no change
+
+ $this->assertEquals('abcd', (string) $str);
+ }
+
+ public function testToIntReturnsMinusOneForPowerOf63()
+ {
+ $str = new StringHelper((string) pow(2, 63));
+ $this->assertEquals(-1, $str->toInt());
+ }
+
+ public function testToIntReturnsMinusOneForPowerOf64()
+ {
+ $str = new StringHelper((string) pow(2, 64));
+ $this->assertEquals(-1, $str->toInt());
+ }
+
+ public function testToIntReturnsMinusOneForValueGreaterThan2Power31()
+ {
+ $str = new StringHelper((string) (pow(2, 31) + 10));
+ $this->assertEquals(-1, $str->toInt());
+ }
+
+ public function testToIntReturnsNormalIntegerWhenWithinRange()
+ {
+ $str = new StringHelper('12345');
+ $this->assertEquals(12345, $str->toInt());
+ }
+
+ public function testFilterDigitsRemovesNonDigits()
+ {
+ $str = new StringHelper('abc123xyz');
+ $result = $str->filterDigits();
+
+ $this->assertEquals('123', (string) $result);
+ }
+
+ public function testFilterDigitsKeepsOnlyDigits()
+ {
+ $str = new StringHelper('987654');
+ $result = $str->filterDigits();
+
+ $this->assertEquals('987654', (string) $result);
+ }
+
+ public function testFilterDigitsRemovesAllWhenNoDigits()
+ {
+ $str = new StringHelper('no digits!');
+ $result = $str->filterDigits();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testFilterDigitsOnEmptyString()
+ {
+ $str = new StringHelper('');
+ $result = $str->filterDigits();
+
+ $this->assertEquals('', (string) $result);
+ }
+
+ public function testUnescapeRevertsEscapedCharacters()
+ {
+ // Example: TS3 escaped “ ” → “\s”, “|” → “\p”
+ $str = new StringHelper('Hello\sWorld\pServer');
+ $result = $str->unescape();
+
+ $this->assertEquals('Hello World|Server', (string) $result);
+ }
+
+ public function testUnescapeWithNoEscapes()
+ {
+ $str = new StringHelper('NoEscapesHere');
+ $result = $str->unescape();
+
+ $this->assertEquals('NoEscapesHere', (string) $result);
+ }
+
+ public function testUnescapeReturnsSelf()
+ {
+ $str = new StringHelper('Foo\sBar');
+ $result = $str->unescape();
+
+ $this->assertSame($result, $result);
+ }
}
diff --git a/tests/Helper/UriTest.php b/tests/Helper/UriTest.php
index 2fb7184e..e901001d 100644
--- a/tests/Helper/UriTest.php
+++ b/tests/Helper/UriTest.php
@@ -7,6 +7,7 @@
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\HelperException;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Uri;
+use ReflectionMethod;
class UriTest extends TestCase
{
@@ -485,4 +486,107 @@ public function testGetFragment(Uri $uri)
$uri->getFragment()
);
}
+
+ /**
+ * @throws HelperException
+ */
+ public function testCheckCatchesException()
+ {
+ $uri = new StringHelper(''); // empty string → throws HelperException in constructor
+ $this->assertFalse(Uri::check($uri));
+ }
+
+ /**
+ * @throws HelperException
+ */
+ public function testCheckHandlesValidConstruction()
+ {
+ $uri = new StringHelper('http://example.com');
+
+ // We don't know what isValid() returns,
+ // so we just check that no exception is thrown and that the return value is a bool.
+ $result = Uri::check($uri);
+
+ $this->assertIsBool($result);
+ }
+
+ /**
+ * @throws HelperException
+ */
+ public function testCheckReturnsFalseWhenConstructorThrows()
+ {
+ // We simulate a URI that causes the Uri constructor to fail
+ // e.g., an empty or invalid string
+ $uri = new StringHelper('');
+
+ // Damit testen wir den catch-Block
+ $this->assertFalse(Uri::check($uri));
+ }
+
+ public function testGetFQDNPartsReturnsEmptyArrayForInvalidHostname()
+ {
+ // Contains invalid characters (e.g., spaces or special characters)
+ $result = Uri::getFQDNParts('invalid host name');
+ $this->assertSame([], $result);
+ }
+
+ public function testGetFQDNPartsReturnsPartsForValidHostname()
+ {
+ $result = Uri::getFQDNParts('sub.example.com');
+
+ // Check that the array contains the expected keys
+ $this->assertArrayHasKey('tld', $result);
+ $this->assertArrayHasKey('2nd', $result);
+ $this->assertArrayHasKey('3rd', $result);
+
+ // Example values (depending on regex matching)
+ $this->assertEquals('com', $result['tld']);
+ $this->assertEquals('example.', $result['2nd']); // Note: Regex contains a period!
+ $this->assertEquals('sub.', $result['3rd']);
+ }
+
+ /**
+ * @throws \ReflectionException
+ */
+ protected function callProtectedStatic(string $method, array $args = [])
+ {
+ $ref = new ReflectionMethod(Uri::class, $method);
+ /** @noinspection PhpExpressionResultUnusedInspection */
+ $ref->setAccessible(true);
+
+ return $ref->invokeArgs(null, $args);
+ }
+
+ /**
+ * @throws \ReflectionException
+ */
+ public function testStripslashesRecursiveRemovesSlashesFromString()
+ {
+ $result = $this->callProtectedStatic('stripslashesRecursive', ['A\\B']);
+ $this->assertEquals('AB', $result);
+ }
+
+ /**
+ * @throws \ReflectionException
+ */
+ public function testStripslashesRecursiveHandlesFlatArray()
+ {
+ $input = ['x\\y', 'a\\b'];
+ $expected = ['xy', 'ab'];
+
+ $result = $this->callProtectedStatic('stripslashesRecursive', [$input]);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @throws \ReflectionException
+ */
+ public function testStripslashesRecursiveHandlesNestedArrays()
+ {
+ $input = ['outer' => ['inner' => 'x\\y\\z']];
+ $expected = ['outer' => ['inner' => 'xyz']];
+
+ $result = $this->callProtectedStatic('stripslashesRecursive', [$input]);
+ $this->assertEquals($expected, $result);
+ }
}