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 = "
%5%8 %9%11%12
\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); + } }