. * * @package TeamSpeak3 * @author Sven 'ScP' Paulsen * @copyright Copyright (c) Planet TeamSpeak. All rights reserved. */ namespace PlanetTeamSpeak\TeamSpeak3Framework\Node; use ArrayAccess; use Countable; use PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery; use PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery\Reply; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\AdapterException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\NodeException; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException; use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Convert; use PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper; use PlanetTeamSpeak\TeamSpeak3Framework\Viewer\ViewerInterface; use RecursiveIterator; use RecursiveIteratorIterator; /** * Class Node * @package PlanetTeamSpeak\TeamSpeak3Framework\Node * @class Abstract * @brief Abstract class describing a TeamSpeak 3 node and all it's parameters. */ abstract class Node implements RecursiveIterator, ArrayAccess, Countable { /** * @var Node|ServerQuery|null */ protected Node|null|ServerQuery $parent = null; /** * @ignore */ protected array|null $server = null; /** * @ignore */ protected int $nodeId = 0x00; /** * @ignore */ protected array|null $nodeList = null; /** * @ignore */ protected array $nodeInfo = []; /** * @ignore */ protected array $storage = []; /** * Sends a prepared command to the server and returns the result. * * @param string $cmd * @param boolean $throw * @return Reply * @throws AdapterException|ServerQueryException */ public function request(string $cmd, bool $throw = true): Reply { return $this->getParent()->request($cmd, $throw); } /** * Uses given parameters and returns a prepared ServerQuery command. * * @param string $cmd * @param array $params * @return StringHelper */ public function prepare(string $cmd, array $params = []): StringHelper { return StringHelper::factory($this->getParent()->prepare($cmd, $params)); } /** * Prepares and executes a ServerQuery command and returns the result. * * @param $cmd * @param array $params * @return Reply * @throws AdapterException * @throws ServerQueryException */ public function execute($cmd, array $params = []): Reply { return $this->request($this->prepare($cmd, $params)); } /** * Returns the parent object of the current node. * * @return ServerQuery|Node|null */ public function getParent(): ServerQuery|Node|null { return $this->parent; } /** * Returns the primary ID of the current node. * * @return integer */ public function getId(): int { return $this->nodeId; } /** * Returns TRUE if the node icon has a local source. * * @param string $key * @return boolean */ public function iconIsLocal(string $key): bool { $iconid = $this[$key]; if (!is_int($iconid)) { $iconid = $iconid->toInt(); } return $iconid > 0 && $iconid < 1000; } /** * Returns the internal path of the node icon. * * @param string $key * @return StringHelper */ public function iconGetName(string $key): StringHelper { $iconid = $this[$key]; if (!is_int($iconid)) { $iconid = $iconid->toInt(); } $iconid = ($iconid < 0) ? (pow(2, 32)) - ($iconid * -1) : $iconid; return new StringHelper("/icon_" . $iconid); } /** * 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. * * @return string */ abstract public function getIcon(): string; /** * Returns a symbol representing the node. * * @return 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 { $html = $viewer->fetchObject($this); $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $node) { $siblings = []; for ($level = 0; $level < $iterator->getDepth(); $level++) { $siblings[] = ($iterator->getSubIterator($level)->hasNext()) ? 1 : 0; } $siblings[] = (!$iterator->getSubIterator($level)->hasNext()) ? 1 : 0; $html .= $viewer->fetchObject($node, $siblings); } if (empty($html) && method_exists($viewer, "toString")) { return $viewer->toString(); } return $html; } /** * Filters given node list array using specified filter rules. * * @param array $nodes * @param array $rules * @return array */ protected function filterList(array $nodes = [], array $rules = []): array { if (!empty($rules)) { foreach ($nodes as $node) { if (!$node instanceof Node) { continue; } $props = $node->getInfo(false); $props = array_intersect_key($props, $rules); foreach ($props as $key => $val) { if ($val instanceof StringHelper) { $match = $val->contains($rules[$key], true); } else { $match = $val == $rules[$key]; } if ($match === false) { unset($nodes[$node->getId()]); } } } } return $nodes; } /** * Returns all information available on this node. If $convert is enabled, some property * values will be converted to human-readable values. * * @param boolean $extend * @param boolean $convert * @return array */ public function getInfo(bool $extend = true, bool $convert = false): array { if ($extend) { $this->fetchNodeInfo(); } if ($convert) { $info = $this->nodeInfo; foreach ($info as $key => $val) { $key = StringHelper::factory($key); if ($key->contains("_bytes_")) { $info[$key->toString()] = Convert::bytes($val); } elseif ($key->contains("_bandwidth_")) { $info[$key->toString()] = Convert::bytes($val) . "/s"; } elseif ($key->contains("_packets_")) { $info[$key->toString()] = number_format($val, 0, null, "."); } elseif ($key->contains("_packetloss_")) { $info[$key->toString()] = sprintf("%01.2f", floatval($val instanceof StringHelper ? $val->toString() : strval($val)) * 100) . "%"; } elseif ($key->endsWith("_uptime")) { $info[$key->toString()] = Convert::seconds($val); } elseif ($key->endsWith("_version")) { $info[$key->toString()] = Convert::version($val); } elseif ($key->endsWith("_icon_id")) { $info[$key->toString()] = $this->iconGetName($key)->filterDigits(); } } return $info; } return $this->nodeInfo; } /** * Returns the specified property or a pre-defined default value from the node info array. * * @param string $property * @param mixed|null $default * @return mixed */ public function getProperty(string $property, mixed $default = null): mixed { if (!$this->offsetExists($property)) { $this->fetchNodeInfo(); } if (!$this->offsetExists($property)) { return $default; } return $this->nodeInfo[$property]; } /** * Returns a string representation of this node. * * @return string */ public function __toString() { return get_class($this); } /** * Returns a string representation of this node. * * @return string */ public function toString(): string { return $this->__toString(); } /** * Returns an assoc array filled with current node info properties. * * @return array */ public function toArray(): array { return $this->nodeList; } /** * Called whenever we're using an unknown method. * * @param string $name * @param array $args * @return mixed * @throws NodeException */ public function __call(string $name, array $args) { if ($this->getParent() instanceof Node) { return call_user_func_array([$this->getParent(), $name], $args); } throw new NodeException("node method '" . $name . "()' does not exist"); } /** * Writes data to the internal storage array. * * @param string $key * @param mixed $val * @return void */ protected function setStorage(string $key, mixed $val): void { $this->storage[$key] = $val; } /** * Returns data from the internal storage array. * * @param string $key * @param mixed|null $default * @return mixed */ protected function getStorage(string $key, mixed $default = null): mixed { return !empty($this->storage[$key]) ? $this->storage[$key] : $default; } /** * Deletes data from the internal storage array. * * @param string $key * @return void */ protected function delStorage(string $key): void { unset($this->storage[$key]); } /** * Commit pending data. * * @return array */ public function __sleep() { return ["parent", "storage", "nodeId"]; } /** * @ignore */ protected function fetchNodeList() { $this->nodeList = []; } /** * @ignore */ protected function fetchNodeInfo() { } /** * @ignore */ protected function resetNodeInfo() { $this->nodeInfo = []; } /** * @ignore */ protected function verifyNodeList() { if ($this->nodeList === null) { $this->fetchNodeList(); } } /** * @ignore */ protected function resetNodeList() { $this->nodeList = null; } /** * @ignore */ public function count(): int { $this->verifyNodeList(); return count($this->nodeList); } /** * @ignore */ public function current(): mixed { $this->verifyNodeList(); return current($this->nodeList); } /** * @ignore */ public function getChildren(): null|RecursiveIterator { $this->verifyNodeList(); return $this->current(); } /** * @ignore */ public function hasChildren(): bool { $this->verifyNodeList(); return $this->current()->count() > 0; } /** * @ignore */ public function hasNext(): bool { $this->verifyNodeList(); return $this->key() + 1 < $this->count(); } /** * @ignore */ public function key(): string|int|null { $this->verifyNodeList(); return key($this->nodeList); } /** * @ignore */ public function valid(): bool { $this->verifyNodeList(); return $this->key() !== null; } /** * @ignore */ public function next(): void { $this->verifyNodeList(); next($this->nodeList); } /** * @ignore */ public function rewind(): void { $this->verifyNodeList(); reset($this->nodeList); } /** * @ignore */ public function offsetExists($offset): bool { return array_key_exists((string)$offset, $this->nodeInfo); } /** * @throws NodeException * @ignore */ public function offsetGet($offset): mixed { if (!$this->offsetExists($offset)) { $this->fetchNodeInfo(); } if (!$this->offsetExists($offset)) { throw new NodeException("node '" . get_class($this) . "' has no property named '" . $offset . "'"); } return $this->nodeInfo[(string)$offset]; } /** * @throws NodeException * @ignore */ public function offsetSet($offset, $value): void { if (method_exists($this, "modify")) { $this->modify([(string)$offset => $value]); return; } throw new NodeException("node '" . get_class($this) . "' is read only"); } /** * @ignore */ public function offsetUnset($offset): void { unset($this->nodeInfo[(string)$offset]); } /** * @throws NodeException * @ignore */ public function __get($offset) { return $this->offsetGet($offset); } /** * @throws NodeException * @ignore */ public function __set($offset, $value) { $this->offsetSet($offset, $value); } }