First release of 2.0! :D

This commit is contained in:
Wruczek
2018-12-27 18:59:49 +01:00
parent 7396a76816
commit 628af52b54
293 changed files with 12641 additions and 3 deletions

View File

@ -0,0 +1,143 @@
<?php
namespace Wruczek\TSWebsite;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\TeamSpeakUtils;
/**
* Class used to generate an array with statuses of admins, used
* later to power the "Admin status" feature in the sidebar
*/
class AdminStatus {
use Utils\SingletonTait;
private $cache;
const STATUS_STYLE_GROUPED = 1;
const STATUS_STYLE_GROUPED_HIDE_EMPTY_GROUPS = 2;
const STATUS_STYLE_LIST = 3;
const STATUS_STYLE_LIST_ONLINE_FIRST = 4;
public function __construct() {
$this->cache = new PhpFileCache(__CACHE_DIR, "adminstatus");
}
public function getCachedAdminClients(array $adminGroups) {
return $this->cache->refreshIfExpired("adminstatus", function () use ($adminGroups) {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
$nodeServer = TeamSpeakUtils::i()->getTSNodeServer();
$clients = [];
foreach ($adminGroups as $groupId) {
$clients[$groupId] = $nodeServer->serverGroupClientList($groupId);
}
return $clients;
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_adminstatus"));
}
public function getStatus(array $adminGroups, $format, $hideOffline = false, array $ignoredUsersDbids = []) {
if ($format !== self::STATUS_STYLE_GROUPED
&& $format !== self::STATUS_STYLE_GROUPED_HIDE_EMPTY_GROUPS
&& $format !== self::STATUS_STYLE_LIST
&& $format !== self::STATUS_STYLE_LIST_ONLINE_FIRST) {
throw new \InvalidArgumentException("Invalid format specified");
}
$serverGroupList = CacheManager::i()->getServerGroupList();
$adminStatus = $this->getCachedAdminClients($adminGroups);
$data = [];
if ($serverGroupList === null || $adminStatus === null) {
// if we dont have a server group list or the
// cached admin clients, we cannot do anything
// (its probably a connection issue)
// false means "data problem"
return false;
}
foreach ($adminGroups as $adminGroupId) {
// try to get that group from server group list
$serverGroup = @$serverGroupList[$adminGroupId];
// skip if we cant get that group
if ($serverGroup === null) {
continue;
}
$groupClients = [];
$cachedClients = $adminStatus[$adminGroupId];
foreach ($cachedClients as $client) {
$cldbid = $client["cldbid"];
if (in_array($cldbid, $ignoredUsersDbids)) {
continue;
}
$onlineClient = CacheManager::i()->getClient($cldbid);
if ($format === self::STATUS_STYLE_LIST_ONLINE_FIRST) {
// in list style, inside of data we have
// 2 additional arrays: online and offline
// we add online users to online, and offline users to offline
// at the end, we combine both arrays
$data[$onlineClient ? "online" : "offline"][] = [
"client" => $onlineClient ?: $client,
"group" => $serverGroup
];
} else {
// when dealing with other formats...
if ($onlineClient !== null) {
// if online, add everything from the $onlineClient
$groupClients[] = $onlineClient;
} else if (!$hideOffline) {
// if offline, we only have info from $client returned by the server group list
$groupClients[] = $client;
}
}
}
// sort clients, always show online first
if ($format !== self::STATUS_STYLE_LIST_ONLINE_FIRST) {
uasort($groupClients, function ($a, $b) {
if (isset($a["clid"], $b["clid"])) {
return 0;
}
return isset($a["clid"]) ? -1 : 1;
});
// add sorted data to our results
$data[$adminGroupId] = $serverGroup + ["clients" => $groupClients];
}
}
// in the online first format...
if ($format === self::STATUS_STYLE_LIST_ONLINE_FIRST) {
if ($hideOffline) {
// we dont care about the offline users if hideOffline is true
$data = @$data["online"];
} else {
// ...combine online and offline arrays
// see line #89 for explanation
// online users go before offline users
// NOTE: we are using array_merge instead of the "+"
// operator, because we have default numeric keys.
// Using "+" would make us loose some data
$data = array_merge(@$data["online"], @$data["offline"]);
}
}
return ["format" => $format, "data" => $data];
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace Wruczek\TSWebsite;
use Wruczek\TSWebsite\Utils\TeamSpeakUtils;
class Assigner {
public static function getAssignerConfig() {
return Config::get("assignerconfig");
}
public static function getAssignerArray() {
$assignerConfig = self::getAssignerConfig();
if (empty($assignerConfig)) {
return []; // not configured, do not get more data and just return an empty array
}
$userGroups = Auth::getUserServerGroupIds();
$serverGroups = CacheManager::i()->getServerGroupList();
foreach ($assignerConfig as $index => $category) {
$assignedCount = 0;
$groups = [];
foreach ($category["groups"] as $sgid) {
$serverGroup = @$serverGroups[$sgid];
if ($serverGroup === null) {
continue;
}
$assigned = in_array($sgid, $userGroups);
$groups[$sgid] = $serverGroup + ["assigned" => $assigned];
if ($assigned) {
$assignedCount++;
}
}
$assignerConfig[$index]["assignedCount"] = $assignedCount;
$assignerConfig[$index]["groups"] = $groups;
}
return $assignerConfig;
}
public static function isAssignable($sgid) {
foreach (self::getAssignerConfig() as $category) {
if (in_array($sgid, $category["groups"], true)) {
return true;
}
}
return false;
}
/**
* Attempts to change user groups with the provided $newGroups
* @param array $newGroups array of new SGIDs that the user should
* have. any assigner groups not present in this array will
* be removed from the user
* @return int status code that shows result of the group change.
* 0 - groups have been successfully changed
* 1 - no change between current groups and submitted groups
* 2 - group assigner is not configured, stopping
* 3 - reached category group limit
* @throws UserNotAuthenticatedException
* @throws \TeamSpeak3_Exception
*/
public static function changeGroups($newGroups) {
$assignerConfig = self::getAssignerConfig();
if (empty($assignerConfig)) {
return 2; // if the assigner is not configured, stop there
}
$userGroups = Auth::getUserServerGroupIds();
$groupsToAdd = [];
$groupsToRemove = [];
foreach ($assignerConfig as $config) {
$groupsToAssign = 0;
foreach ($config["groups"] as $group) {
// true if the $group is currently assigned to the user
$isAssigned = in_array($group, $userGroups);
// true if the user wants to be added to $group
$wantToAssign = in_array($group, $newGroups);
// if the group is already assigned, or is to be assigned,
// check for the max group limit in this category:
// - add 1 to the "groupsToAssign", and
// - check if its bigger than the max limit
if ($wantToAssign && (++$groupsToAssign > $config["max"])) {
return 3;
}
// ADD GROUP if the group is not assigned, but the user wants to be added
if (!$isAssigned && $wantToAssign) {
// ok, seems like we can add this group!
$groupsToAdd[] = $group;
}
// REMOVE GROUP if the group is currently assigned, but the user does not want to be inside it
if ($isAssigned && !$wantToAssign) {
$groupsToRemove[] = $group;
}
}
}
// empty arrays - nothing to change
if (!$groupsToAdd && !$groupsToRemove) {
return 1;
}
// finally, add or remove the groups
$tsServer = TeamSpeakUtils::i()->getTSNodeServer();
foreach ($groupsToAdd as $sgid) {
try {
$tsServer->serverGroupClientAdd($sgid, Auth::getCldbid());
} catch (\TeamSpeak3_Exception $e) {} // TODO log it to the admin panel?
}
foreach ($groupsToRemove as $sgid) {
try {
$tsServer->serverGroupClientDel($sgid, Auth::getCldbid());
} catch (\TeamSpeak3_Exception $e) {} // TODO log it to the admin panel?
}
return 0;
}
}

288
src/private/php/Auth.php Normal file
View File

@ -0,0 +1,288 @@
<?php
namespace Wruczek\TSWebsite;
use function array_filter;
use function array_keys;
use Exception;
use function in_array;
use function time;
use function var_dump;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\Language\LanguageUtils;
use Wruczek\TSWebsite\Utils\TeamSpeakUtils;
use Wruczek\TSWebsite\Utils\Utils;
class Auth {
public static function isLoggedIn() {
return !empty(self::getCldbid()) && !empty(self::getUid());
}
public static function getUid() {
return @$_SESSION["tsuser"]["uid"];
}
public static function getCldbid() {
return @$_SESSION["tsuser"]["cldbid"];
}
public static function getNickname() {
return @$_SESSION["tsuser"]["nickname"];
}
public static function logout() {
unset($_SESSION["tsuser"]);
}
public static function getTsUsersByIp($ip = null) {
if ($ip === null) {
$ip = Utils::getClientIp();
}
$clientList = CacheManager::i()->getClientList();
if ($clientList === null) {
return null;
}
$ret = [];
foreach ($clientList as $client) {
// Skip query clients
if ($client["client_type"]) continue;
if ((string) $client["connection_client_ip"] === $ip) {
$ret[$client["client_database_id"]] = (string) $client["client_nickname"];
}
}
return $ret;
}
/**
* Returns true if the $cldbid is connected with the same IP address as $ip
* @param $cldbid int cldbid to check
* @param $ip string optional, defaults to Utils::getClientIp
* @return bool true if the cldbid have the same IP address as $ip
*/
public static function checkClientIp($cldbid, $ip = null) {
if ($ip === null) {
$ip = Utils::getClientIp();
}
$users = self::getTsUsersByIp($ip);
if ($users === null) {
return false;
}
return array_key_exists($cldbid, $users);
}
/**
* Tries to generate and send confirmation code to the TS client
* @param $cldbid int
* @param $poke bool|null true = poke user, false = send a message, null = default value from config
* @return string|null|false Returns code as string on success, null when
* client cannot be found and false when other error occurs.
*/
public static function generateConfirmationCode($cldbid, $poke = null) {
if ($poke === null) {
$poke = (bool) Config::get("loginpokeclient");
}
if (TeamSpeakUtils::i()->checkTSConnection()) {
try {
$client = TeamSpeakUtils::i()->getTSNodeServer()->clientGetByDbid($cldbid);
$code = (string) mt_rand(100000, 999999); // TODO: replace it with a CSPRNG
$msg = LanguageUtils::tl("LOGIN_CONFIRMATION_CODE", $code);
if ($poke) {
$client->poke(mb_substr($msg, 0, 100)); // Max 100 characters for pokes
} else {
$client->message(mb_substr($msg, 0, 1024)); // Max 1024 characters for messages
}
self::saveConfirmationCode($cldbid, $code);
return $code;
} catch (\TeamSpeak3_Adapter_ServerQuery_Exception $e) {
if ($e->getCode() === 512) {
return null;
}
}
}
return false;
}
/**
* Checks if there is already a confirmation code cached for this user.
* Returns the code of found, otherwise NULL.
* @param $cldbid int
* @return string|null Confirmation code, null if not found
*/
public static function getConfirmationCode($cldbid) {
return (new PhpFileCache(__CACHE_DIR, "confirmationcodes"))->retrieve("c_$cldbid");
}
/**
* Saves confirmation code for the user
* @param $cldbid int
* @param $code string
*/
public static function saveConfirmationCode($cldbid, $code) {
(new PhpFileCache(__CACHE_DIR, "confirmationcodes"))->store("c_$cldbid", $code, (int) Config::get("cache_logincode"));
}
/**
* Deletes confirmation code for the user
* @param $cldbid int
*/
public static function deleteConfirmationCode($cldbid) {
(new PhpFileCache(__CACHE_DIR, "confirmationcodes"))->eraseKey("c_$cldbid");
}
/**
* Checks confirmation code and logs user in if its correct.
* @param $cldbid
* @param $userCode
* @return bool true if authentication was successful
*/
public static function checkCodeAndLogin($cldbid, $userCode) {
if (!is_int($cldbid)) {
throw new \InvalidArgumentException("cldbid must be an int");
}
$codeCheck = self::checkConfirmationCode($cldbid, $userCode);
if ($codeCheck !== true) {
return false;
}
$login = self::loginUser($cldbid);
if ($login) {
self::deleteConfirmationCode($cldbid);
return true;
}
return false;
}
/**
* Checks if the provided confirmation code matches the saved one and returns true on success.
* @param $cldbid int
* @param $userCode string
* @return bool
*/
public static function checkConfirmationCode($cldbid, $userCode) {
$knownCode = self::getConfirmationCode($cldbid);
if ($knownCode === null) {
return false;
}
return hash_equals($knownCode, $userCode);
}
/**
* Logins user to this account
* @param $cldbid int
* @return bool true on success, false otherwise
*/
public static function loginUser($cldbid) {
$clientList = CacheManager::i()->getClientList();
foreach ($clientList as $client) {
if ($client["client_database_id"] === $cldbid) {
$_SESSION["tsuser"]["uid"] = (string) $client["client_unique_identifier"];
$_SESSION["tsuser"]["cldbid"] = $client["client_database_id"];
$_SESSION["tsuser"]["nickname"] = (string) $client["client_nickname"];
return true;
}
}
return false;
}
public static function invalidateUserGroupCache() {
unset($_SESSION["tsuser"]["servergroups"]);
}
/**
* Returns an array containing cached array with group IDs of the user
* @param $cacheTime int for how long we should cache the IDs?
* @return array array with server group IDs of the user
* @throws UserNotAuthenticatedException if user is not logged in
* @throws \TeamSpeak3_Exception when we cannot get data from the TS server
*/
public static function getUserServerGroupIds($cacheTime = 60) {
if (!self::isLoggedIn()) {
throw new UserNotAuthenticatedException("User is not authenticated");
}
// Check if we data is already cached and if we can use it
if (isset($_SESSION["tsuser"]["servergroups"])) {
$cached = $_SESSION["tsuser"]["servergroups"];
// Calculate how old is the cached data (in seconds)
$secondsSinceCache = time() - $cached["timestamp"];
// If we dont need to refresh it, return the data
if ($secondsSinceCache <= $cacheTime) {
return $cached["data"];
}
}
// If we end up here, it means we need to refresh the cache
if (!TeamSpeakUtils::i()->checkTSConnection()) {
throw new \TeamSpeak3_Exception("Cannot connect to the TeamSpeak server");
}
try {
$tsServer = TeamSpeakUtils::i()->getTSNodeServer();
// Get all user groups from TS server
$serverGroups = $tsServer->clientGetServerGroupsByDbid(self::getCldbid());
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
throw $e;
}
// Since the array in indexed with server group ID's, we can just separate the keys
// That gives us an array with ID's if user groups
$serverGroupIds = array_keys($serverGroups);
// Cache it in session with current time for later cachebusting
$_SESSION["tsuser"]["servergroups"] = [
"timestamp" => time(),
"data" => $serverGroupIds
];
return $serverGroupIds;
}
/**
* Combines sever group ID's from getUserServerGroupIds() with cached
* server group list and returns full array with user's server groups
* @see self::getUserServerGroupIds
* @param int $cacheTime value passed to getUserServerGroupIds()
* @return array array with user server groups
* @throws UserNotAuthenticatedException if user is not logged in
* @throws \TeamSpeak3_Exception when we cannot get data from the TS server
*/
public static function getUserServerGroups($cacheTime = 60) {
$serverGroupIds = self::getUserServerGroupIds($cacheTime);
$serverGroups = CacheManager::i()->getServerGroupList();
$resut = array_filter($serverGroups, function ($serverGroup) use ($serverGroupIds) {
// If the group id is inside $serverGroupIds,
// keep that group. Otherwise filter it out.
return in_array($serverGroup["sgid"], $serverGroupIds);
});
return $resut;
}
}
class UserNotAuthenticatedException extends \Exception {}

View File

@ -0,0 +1,160 @@
<?php
namespace Wruczek\TSWebsite;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\SingletonTait;
use Wruczek\TSWebsite\Utils\TeamSpeakUtils;
class CacheManager {
use SingletonTait;
private $cache;
private $serverInfo;
private $banList;
private $clientList;
private $channelList;
private $serverGroupList;
private $channelGroupList;
public function __construct() {
$this->cache = new PhpFileCache(__CACHE_DIR, "cachemanager");
}
private function tsNodeObjectToArray(array $object, $extendInfo = false) {
if (!is_array($object)) {
throw new \Exception("object must be a array filled with TeamSpeak3_Node_Abstract objects");
}
$data = [];
foreach ($object as $obj) {
$data[$obj->getId()] = $obj->getInfo($extendInfo);
}
return $data;
}
public function getServerInfo($meta = false) {
if ($this->serverInfo) {
return $this->serverInfo;
}
return $this->serverInfo = $this->cache->refreshIfExpired("serverinfo", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return TeamSpeakUtils::i()->getTSNodeServer()->getInfo();
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_serverinfo"), $meta);
}
public function getBanList($meta = false) {
if ($this->banList) {
return $this->banList;
}
return $this->banList = $this->cache->refreshIfExpired("banlist", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return TeamSpeakUtils::i()->getTSNodeServer()->banList();
} catch (\TeamSpeak3_Exception $e) {
if ($e->getCode() === 1281) { // database empty result set
return [];
}
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_banlist"), $meta);
}
public function getClientList($meta = false) {
if ($this->clientList) {
return $this->clientList;
}
return $this->clientList = $this->cache->refreshIfExpired("clientlist", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return $this->tsNodeObjectToArray(TeamSpeakUtils::i()->getTSNodeServer()->clientList());
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_clientlist"), $meta); // Lower cache time because of login system
}
public function getClient($cldbid) {
$clients = $this->getClientList();
if ($clients === null) {
return null;
}
foreach ($clients as $client) {
if ($client["client_database_id"] === $cldbid) {
return $client;
}
}
return null;
}
public function getChannelList($meta = false) {
if ($this->channelList) {
return $this->channelList;
}
return $this->channelList = $this->cache->refreshIfExpired("channellist", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return $this->tsNodeObjectToArray(TeamSpeakUtils::i()->getTSNodeServer()->channelList());
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_channelist"), $meta);
}
public function getServerGroupList($meta = false) {
if ($this->serverGroupList) {
return $this->serverGroupList;
}
return $this->serverGroupList = $this->cache->refreshIfExpired("servergrouplist", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return $this->tsNodeObjectToArray(TeamSpeakUtils::i()->getTSNodeServer()->serverGroupList());
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_servergroups"), $meta);
}
public function getChannelGroupList($meta = false) {
if ($this->channelGroupList) {
return $this->channelGroupList;
}
return $this->channelGroupList = $this->cache->refreshIfExpired("channelgrouplist", function () {
if(TeamSpeakUtils::i()->checkTSConnection()) {
try {
return $this->tsNodeObjectToArray(TeamSpeakUtils::i()->getTSNodeServer()->channelGroupList());
} catch (\TeamSpeak3_Exception $e) {
TeamSpeakUtils::i()->addExceptionToExceptionsList($e);
}
}
return null;
}, Config::get("cache_channelgroups"), $meta);
}
}

179
src/private/php/Config.php Normal file
View File

@ -0,0 +1,179 @@
<?php
namespace Wruczek\TSWebsite;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\DatabaseUtils;
use Wruczek\TSWebsite\Utils\TemplateUtils;
/**
* Class Config
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class Config {
use Utils\SingletonTait;
protected $databaseConfig;
protected $config;
protected $cache;
public static function get($key, $default = null) {
return self::i()->getValue($key, $default);
}
private function __construct() {
if(!defined("__CONFIG_FILE")) {
die("__CONFIG_FILE is not defined");
}
$config = require __CONFIG_FILE;
if($config === null || !is_array($config)) {
die("Cannot read the db config file! (" . __CONFIG_FILE . ")");
}
$this->databaseConfig = $config;
$this->cache = new PhpFileCache(__CACHE_DIR, "config");
}
/**
* Returns config used to connect to the database
* @return array Config as an array
*/
public function getDatabaseConfig() {
return $this->databaseConfig;
}
/**
* Returns configuration saved in database
* @return array Config file as an key => value array
*/
public function getConfig() {
if($this->config === null) {
$this->config = $this->cache->refreshIfExpired("config", function () {
try {
$db = DatabaseUtils::i()->getDb();
$data = $db->select("config", ["identifier", "type", "value"]);
} catch (\Exception $e) {
TemplateUtils::i()->renderErrorTemplate("DB error", "Cannot get config data from database", $e->getMessage());
exit;
}
if(!empty($db->error()[1])) {
return null;
}
$cfg = [];
foreach ($data as $item) {
$key = $item["identifier"];
$type = $item["type"];
$val = $item["value"];
switch ($type) {
case "STRING":
$val = (string) $val;
break;
case "INT":
$val = (int) $val;
break;
case "FLOAT":
$val = (float) $val;
break;
case "BOOL":
$val = strtolower($val) === "true";
break;
case "JSON":
$json = json_decode((string) $val, true);
if ($json === false) {
throw new \Exception("Error loading config from db: cannot parse JSON from $key");
}
$val = $json;
break;
default:
throw new \Exception("Error loading config from db: unrecognised data type $type");
}
$cfg[$key] = $val;
}
return $cfg;
}, 60);
}
return $this->config;
}
/**
* Resets current config cache
*/
public function clearConfigCache() {
$this->config = null;
$this->cache->eraseKey("config");
}
/**
* Returns value associated with given key
* @param string $key
* @param null $default
* @return mixed value Returns string with
* the value if key exists, null otherwise
*/
public function getValue($key, $default = null) {
return isset($this->getConfig()[$key]) ? $this->getConfig()[$key] : $default;
}
/**
* Saves key => value combo in config table
* @param string $key
* @param string|int|float|bool|array|object $value
* @return bool true on success, false otherwise
* @throws \Exception
*/
public function setValue($key, $value) {
$db = DatabaseUtils::i()->getDb();
switch (gettype($value)) {
case "string":
$type = "STRING";
break;
case "integer":
$type = "INT";
break;
case "double":
$type = "FLOAT";
break;
case "boolean":
$type = "BOOL";
$value = $value ? "true" : "false";
break;
case "array":
case "object":
$type = "JSON";
$value = json_encode($value);
break;
default:
throw new \Exception("Unsupported data type");
}
$data = [
"identifier" => $key,
"type" => $type,
"value" => $value
];
if($db->has("config", ["identifier" => $key])) {
$ret = $db->update("config", $data, ["identifier" => $key]);
} else {
$ret = $db->insert("config", $data);
}
$this->clearConfigCache();
return $ret;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Wruczek\TSWebsite\News;
use function mb_substr;
use function time;
use Wruczek\TSWebsite\Utils\DatabaseUtils;
/**
* Class DefaultNewsStore.
* Reads news from the database, they might be added via the admin panel.
* @package Wruczek\TSWebsite\News
*/
class DefaultNewsStore implements INewsStore {
private $db;
private $newsTable = "news";
public function __construct() {
$this->db = DatabaseUtils::i()->getDb();
}
public function getNewsList($limit, $offset = null) {
if ($limit !== null && !\is_int($limit)) {
throw new \InvalidArgumentException("limit must be an integer");
}
if ($offset !== null && !\is_int($offset)) {
throw new \InvalidArgumentException("offset must be an integer");
}
$options = []; // Medoo: [$offset, $limit]
// If we have both limit and offset
if ($limit !== null && $offset !== null) {
$options = [$offset, $limit];
} else if ($limit !== null) { // if we have only limit
$options = $limit;
}
$data = $this->db->select($this->newsTable, "*", [
"ORDER" => ["added" => "DESC"],
"LIMIT" => $options
]);
$newsList = [];
foreach ($data as $row) {
$newsId = $row["newsid"];
$newsList[$newsId] = [
"newsId" => $newsId,
"title" => $row["title"],
// There is no separate news pages for now, so we show the entire content as the description
// "description" => mb_substr($row["content"], 0, 200),
"description" => $row["content"],
"added" => $row["added"],
"edited" => $row["edited"],
// "link" => "news.php?id=$newsId",
"external" => false,
];
}
return $newsList;
}
public function getNews($newsId) {
return $this->db->get($this->newsTable, "*", [
"newsId" => $newsId,
]);
}
public function getNewsCount() {
return $this->db->count($this->newsTable);
}
public function addNews($title, $content, $addDate = null, $editDate = null) {
if ($addDate === null) {
$addDate = time();
}
$this->db->insert($this->newsTable, [
"title" => $title,
"added" => $addDate,
"edited" => $editDate,
"content" => $content,
]);
return $this->db->id();
}
public function editNews($newsId, $title = null, $content = null, $addDate = null, $editDate = null) {
$data = [];
if ($title !== null) $data["title"] = $title;
if ($content !== null) $data["content"] = $content;
if ($addDate !== null) $data["added"] = $addDate;
if ($editDate !== null) $data["edited"] = $editDate;
$update = $this->db->update($this->newsTable, $data, [
"newsId" => $newsId
]);
return $update->rowCount() !== 0;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Wruczek\TSWebsite\News;
interface INewsStore {
/**
* Returns array with news, sorted by the latest first. Format:
* <code>
* [
* $newsId = [
* "newsId" => int,
* "title" => "title of the news",
* "description" => "short description",
* "added" => int creation timestamp,
* "edited" => int last edit timestamp, null if never edited,
* "link" => "clicking on the news header will redirect here",
* "external" => true|false if true, the link will open in new tab
* ]
* ]
* </code>
* @param int $limit Number of results to return
* @param int $offset From where to start the list
* @return array array with the news
* @throws \Exception when we cannot get the news
*/
public function getNewsList($limit, $offset = null);
/**
* Returns full information about this particular news
* @param int $newsId
* @return array|null array with the news details or null if news was not found
*/
public function getNews($newsId);
/**
* Returns a number of news in the database
* @return int
*/
public function getNewsCount();
/**
* Adds a new news and return its new id
* @param string $title
* @param string $content
* @param null|int $addDate if null, the implementation will use the current timestamp
* @param null|int $editDate
* @return int newsId of the inserted news
*/
public function addNews($title, $content, $addDate = null, $editDate = null);
/**
* Edit the news selected by $newsId. All parameters are optional, and only the provided ones will be changed
* @param int $newsId
* @param string|null $title
* @param string|null $content
* @param int|null $addDate
* @param int|null $editDate
*/
public function editNews($newsId, $title = null, $content = null, $addDate = null, $editDate = null);
}

View File

@ -0,0 +1,105 @@
<?php
namespace Wruczek\TSWebsite;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\TeamSpeakUtils;
class ServerIconCache {
private static $iconsCacheDir = __CACHE_DIR . "/servericons";
public static function getIconBytes($iconId) {
if (!is_numeric($iconId)) {
throw new \Exception("iconid need to be an int or numeric string");
}
$file = @file_get_contents(self::$iconsCacheDir . "/" . $iconId);
return $file === false ? null : $file; // return null on error
}
public static function hasIcon($iconId) {
return self::getIconBytes($iconId) !== null;
}
public static function syncIcons() {
if (!file_exists(self::$iconsCacheDir) && !mkdir(self::$iconsCacheDir, true)) {
throw new \Exception("Cannot create icons cache directory at " . self::$iconsCacheDir);
}
foreach (self::ftDownloadIconList() as $iconElement) {
$iconName = (string) $iconElement["name"];
$iconId = self::iconIdFromName($iconName);
if (self::hasIcon($iconId)) {
continue;
}
try {
$iconData = self::downloadIcon($iconId);
} catch (\Exception $e) {
trigger_error("Cannot download icon $iconId");
continue;
}
$createFile = file_put_contents(self::$iconsCacheDir . "/$iconId", $iconData);
if ($createFile === false) {
throw new \Exception("Cannot create icon file for icon $iconId, check folder permissions");
}
}
}
public static function syncIfNeeded() {
(new PhpFileCache(__CACHE_DIR))->refreshIfExpired("lasticonsync", function () {
// Do not sync icons if we cannot connect the the TS server
if (!TeamSpeakUtils::i()->checkTSConnection()) {
return null;
}
ServerIconCache::syncIcons();
return true;
}, Config::get("cache_servericons", 300));
}
public static function isLocal($iconId) {
return $iconId > 0 && $iconId < 1000;
}
public static function iconIdFromName($iconName) {
return substr($iconName, 5);
}
/**
* Converts a 32-bit int to a unsigned int
* 32-bit int is obtained for example from the servergroup details (iconid)
* Returned value can be used with ServerIconCache's methods like getIconBytes
* @see http://yat.qa/resources/tools/ (Icon Filename Tool)
* @param $iconId int
* @return int
*/
public static function unsignIcon($iconId) {
if (!is_int($iconId)) {
throw new \InvalidArgumentException("iconId must be an integer");
}
return ($iconId < 0) ? (2 ** 32) - ($iconId * -1) : $iconId;
}
public static function downloadIcon($iconId) {
return TeamSpeakUtils::i()->ftDownloadFile("/icon_$iconId");
}
public static function ftDownloadIconList() {
try {
return TeamSpeakUtils::i()->getTSNodeServer()->channelFileList(0, "", "/icons/");
} catch (\TeamSpeak3_Adapter_ServerQuery_Exception $e) {
if ($e->getCode() === 1281) { // database empty result set
return [];
}
throw $e;
}
}
}

View File

@ -0,0 +1,232 @@
<?php
namespace Wruczek\TSWebsite;
use TeamSpeak3;
use TeamSpeak3_Helper_String;
/**
* Class TeamSpeakChannel
* Some functions copied from ts3-php-framework and modified
*/
class TeamSpeakChannel {
private $channelList;
private $info;
private $clientList;
public function __construct($cid) {
if (is_array($cid)) {
$cid = (int) $cid["cid"];
}
if (!is_int($cid)) {
throw new \InvalidArgumentException("cid needs to be either an channel id (int)" .
" or an array containing key named cid");
}
$this->channelList = CacheManager::i()->getChannelList();
if (!isset($this->channelList[$cid])) {
throw new \InvalidArgumentException("Channel with ID $cid was not found in the channel cache");
}
$this->info = $this->channelList[$cid];
}
private function getChannelList() {
return $this->channelList;
}
private function getClientList() {
if ($this->clientList === null) {
$this->clientList = CacheManager::i()->getClientList();
}
return $this->clientList;
}
public function getInfo() {
return $this->info;
}
public function getId() {
return (int) $this->info["cid"];
}
public function getName() {
return (string) $this->info["channel_name"];
}
public function getDisplayName() {
if ($this->isSpacer()) {
// If its a spacer, remove everything before the
// first "]", and then the "]" itself.
return mb_substr(mb_strstr($this->getName(), "]"), 1);
}
return $this->getName();
}
public function isPermanent() {
return (bool) $this->info["channel_flag_permanent"];
}
public function getParentId() {
return (int) $this->info["pid"];
}
public function isOccupied($checkChildrens = false, $includeQuery = false) {
if ($checkChildrens) {
// Loop through all the children channels, and check if they are occupied
foreach ($this->getChildChannels(true) as $channel) {
if ($channel->isOccupied(false, $includeQuery)) {
return true;
}
}
return false;
}
// We could use the getChannelMembers method:
// return count($this->getChannelMembers($includeQuery)) > 0;
// But its much faster to return on the first instance then to
// count up all users and then compare their number.
foreach ($this->getClientList() as $client) {
if (!$client["client_type"] && $client["cid"] === $this->getId()) {
return true;
}
}
return false;
}
public function hasPassword() {
return $this->info["channel_flag_password"] === 1;
}
public function getTotalClients() {
return (int) $this->info["total_clients"];
}
public function isFullyOccupied() {
return $this->info["channel_maxclients"] !== -1 &&
$this->info["channel_maxclients"] <= $this->info["total_clients"];
}
public function isDefaultChannel() {
return $this->info["channel_flag_default"] === 1;
}
public function isTopChannel() {
return $this->getParentId() === 0;
}
public function getParentChannels($max = -1) {
$pid = (int) $this->info["pid"];
$parents = [];
while ($pid !== 0 && ($max < 0 || count($parents) < $max)) {
$parent = new TeamSpeakChannel($pid);
$parents[$pid] = $parent;
$pid = $parent->getParentId();
}
return $parents;
}
public function getClosestParentChannel() {
$parentChannels = $this->getParentChannels(1);
return isset($parentChannels[0]) ? $parentChannels[0] : null;
}
public function getChildChannels($resursive = false) {
$childList = [];
foreach ($this->getChannelList() as $child) {
if ($child["pid"] === $this->getId()) {
$childTSC = new TeamSpeakChannel($child);
$childList[$childTSC->getId()] = $childTSC;
if ($resursive) {
$childList += $childTSC->getChildChannels(true);
}
}
}
return $childList;
}
public function getClosestChildChannel() {
$childChannels = $this->getChildChannels(1);
return isset($childChannels[0]) ? $childChannels[0] : null;
}
public function getChannelMembers($includeQuery = false) {
$clientList = [];
foreach ($this->getClientList() as $client) {
if ($client["cid"] === $this->getId() && ($includeQuery || !$client["client_type"])) {
// $childTSC = new TeamSpeakClient($child["clid"]);
$clientList[$client["clid"]] = $client;
}
}
return $clientList;
}
public function isSpacer() {
return preg_match("/\[[^\]]*spacer[^\]]*\]/", $this->getName()) && $this->isPermanent() && !$this->getParentId();
}
/**
* Returns the possible alignment of a channel spacer
* @return int|false
*/
public function getSpacerAlign() {
if(!$this->isSpacer() || !preg_match("/\[(.*)spacer.*\]/", $this->getName(), $matches) || !isset($matches[1])) {
return false;
}
if ($this->getSpacerType() !== TeamSpeak3::SPACER_CUSTOM) {
return TeamSpeak3::SPACER_ALIGN_REPEAT;
}
switch($matches[1]) {
case "*":
return TeamSpeak3::SPACER_ALIGN_REPEAT;
case "c":
return TeamSpeak3::SPACER_ALIGN_CENTER;
case "r":
return TeamSpeak3::SPACER_ALIGN_RIGHT;
default:
return TeamSpeak3::SPACER_ALIGN_LEFT;
}
}
public function getSpacerType() {
if(!$this->isSpacer()) {
return false;
}
switch((new TeamSpeak3_Helper_String($this->getName()))->section("]", 1)) {
case "___":
return TeamSpeak3::SPACER_SOLIDLINE;
case "---":
return TeamSpeak3::SPACER_DASHLINE;
case "...":
return TeamSpeak3::SPACER_DOTLINE;
case "-.-":
return TeamSpeak3::SPACER_DASHDOTLINE;
case "-..":
return TeamSpeak3::SPACER_DASHDOTDOTLINE;
default:
return TeamSpeak3::SPACER_CUSTOM;
}
}
public function __toString() {
return $this->getName();
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use function is_array;
use Wruczek\TSWebsite\Auth;
class ApiUtils {
/**
* Checks if the user is logged in, and if not outputs a JSON error and terminates the script
*/
public static function checkAuth() {
if (!Auth::isLoggedIn()) {
self::jsonError("You must be logged in to perform this action", "NOT_AUTHENTICATED", 401);
exit;
}
}
/**
* Calls jsonResponse with true as success parameter
*/
public static function jsonSuccess($data = null, $code = null, $statusCode = null) {
self::jsonResponse(true, $data, $code, $statusCode);
}
/**
* Calls jsonResponse with false as success parameter
*/
public static function jsonError($data = null, $code = null, $statusCode = null) {
self::jsonResponse(false, $data, $code, $statusCode);
}
/**
* Outputs json with key "success" set to the $success parameter.
* If $data is null, it skips it. If data is a string, it adds
* it to the json with key "message".
* If $data is an array, it merges it with the success key.
* Else it sets key "data" to $data
* @param $success bool
* @param $data null|string|array
* @param $code int|string error code
* @param $statusCode int Status code to return. null to not change
*/
public static function jsonResponse($success, $data = null, $code = null, $statusCode = null) {
if (!is_bool($success)) {
throw new \InvalidArgumentException("success must be a boolean");
}
$json = ["success" => $success];
if ($code !== null) {
$json["code"] = $code;
}
if (is_string($data)) {
$json["message"] = $data;
} else if (is_array($data)) {
$json = array_merge($json, $data);
} else if($data !== null) {
$json["data"] = $data;
}
if (is_int($statusCode)) {
@http_response_code($statusCode);
}
self::outputJson($json);
}
public static function outputJson($array) {
@header("Content-Type: application/json");
echo json_encode($array);
}
public static function getPostParam($key) {
return self::getParam($_POST, $key);
}
public static function getGetParam($key) {
return self::getParam($_GET, $key);
}
/**
* Returns $array[$key] if exists, otherwise throws an jsonerror and
* terminates the script
* @param $array array
* @param $key string
* @param $canBeArray bool whenever the data can be an array
* @return mixed
*/
public static function getParam($array, $key, $canBeArray = false) {
if (!isset($array[$key])) {
self::jsonError("Parameter $key is not provided", 400);
exit;
}
$data = $array[$key];
if (is_array($data) && !$canBeArray) {
self::jsonError("Parameter $key cannot be an array", 400);
exit;
}
return $data;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Wruczek\TSWebsite\Utils;
class CsrfUtils {
/**
* Generates and returns a new CSRF token
* @param $length int length in bytes
* @return string generated CSRF token
* @throws \Exception when unable to generate a new token
*/
public static function generateToken($length) {
if (function_exists("random_bytes")) {
$token = bin2hex(random_bytes($length));
} else if (function_exists("mcrypt_create_iv")) {
$token = bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM));
} else {
$token = bin2hex(openssl_random_pseudo_bytes($length));
}
if (!is_string($token) || empty($token)) {
throw new \Exception("Cannot generate new CSRF token");
}
return $token;
}
/**
* Returns the current CSRF Token or creates a new one if needed.
* @return string CSRF token
* @throws \Exception When we cannot generate a new CSRF token
*/
public static function getToken() {
if (isset($_SESSION["csrfToken"])) {
return $_SESSION["csrfToken"];
}
$length = 16; // in bytes
$token = self::generateToken($length);
$_SESSION["csrfToken"] = $token;
return $token;
}
/**
* Compares user-provided $token against the one we have.
* @param $toCheck string token to be checked
* @return bool true if tokens match, false otherwise.
*/
public static function validateToken($toCheck) {
$knownToken = @$_SESSION["csrfToken"];
if ($knownToken === null) {
return false;
}
return hash_equals($knownToken, $toCheck);
}
/**
* Tries to get CSRF token from the request and then compares it.
* If it fails, it returns the error page with message and exits the script.
*/
public static function validateRequest() {
if (isset($_POST["csrf-token"])) {
$csrfToken = $_POST["csrf-token"];
} else if (isset($_SERVER["HTTP_X_CSRF_TOKEN"])) {
$csrfToken = $_SERVER["HTTP_X_CSRF_TOKEN"];
}
if (empty($csrfToken) || !self::validateToken($csrfToken)) {
http_response_code(400);
TemplateUtils::i()->renderErrorTemplate("", "Security error. Please go to the previous page and try again.", "CSRF token mismatch");
exit;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use Medoo\Medoo;
use Wruczek\TSWebsite\Config;
/**
* Class DatabaseUtils
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class DatabaseUtils {
use SingletonTait;
protected $configUtils;
protected $db;
private function __construct() {
$this->configUtils = Config::i();
}
/**
* Returns database object created with data from
* database config. Stores connection for reuse.
* @return \Medoo\Medoo database object
*/
public function getDb() {
if($this->db === null) {
try {
$db = new Medoo($this->configUtils->getDatabaseConfig());
} catch (\Exception $e) {
TemplateUtils::i()->renderErrorTemplate("DB error", "Connection to database failed", $e->getMessage());
exit;
}
$this->db = $db;
}
return $this->db;
}
/**
* Returns true if MysqliDb has been ever initialised. Useful
* for checking if there was a database connection attempt.
* @return bool
*/
public function isInitialised() {
return !empty($this->db);
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use Wruczek\TSWebsite\Utils\Language\LanguageUtils;
class DateUtils {
/**
* Returns current date format based on current user language. If it cannot
* be retrieved, default value is returned
* @return string date format
*/
public function getDateFormat() {
try {
return LanguageUtils::i()->translate("DATE_FORMAT");
} catch (\Exception $e) {
return "d.m.Y";
}
}
/**
* Returns current time format based on current user language. If it cannot
* be retrieved, default value is returned
* @return string time format
*/
public function getTimeFormat() {
try {
return LanguageUtils::i()->translate("TIME_FORMAT");
} catch (\Exception $e) {
return "H:i:s";
}
}
/**
* Returns timestamp formatted to string with format from getDateFormat()
* @param $timestamp
* @return false|string
*/
public function formatToDate($timestamp) {
return date($this->getDateFormat(), $timestamp);
}
/**
* Returns timestamp formatted to string with format from getTimeFormat()
* @param $timestamp
* @return false|string
*/
public function formatToTime($timestamp) {
return date($this->getTimeFormat(), $timestamp);
}
/**
* Returns timestamp formatted with formatToDate() and formatToTime()
* @param $timestamp
* @param string $additional additional date format
* @return false|string
*/
public function formatToDateTime($timestamp, $additional = "") {
return date("{$this->getDateFormat()} {$this->getTimeFormat()} $additional", $timestamp);
}
/**
* Formats timestamp into "time ago" string
* For example, timestamp set to 60 seconds ago will return "1 minute ago"
*
* Taken from StackOverflow: https://stackoverflow.com/a/18602474
* @param $timestamp int timestamp with past date
* @param bool $full if true, full date will be returned. For example "5 hours, 2 minutes, 8 seconds"
* @return string timestamp formatted to fuzzy date. Marf.
*/
public function fuzzyDate($timestamp, $full = false) {
$now = new \DateTime;
$ago = (new \DateTime)->setTimestamp($timestamp);
$diff = $now->diff($ago);
$diff->w = floor($diff->d / 7);
$diff->d -= $diff->w * 7;
$string = [
'y' => 'year',
'm' => 'month',
'w' => 'week',
'd' => 'day',
'h' => 'hour',
'i' => 'minute',
's' => 'second'
];
foreach ($string as $k => &$v) {
if ($diff->$k) {
$v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
} else {
unset($string[$k]);
}
}
if (!$full) $string = array_slice($string, 0, 1);
return $string ? implode(', ', $string) . ' ago' : 'just now';
}
/**
* Returns fuzzy date with abbreviation showing precise date
* @see fuzzyDate
* @param $timestamp
* @param bool $full
* @return string
*/
public function fuzzyDateHTML($timestamp, $full = false) {
$fuzzyDate = $this->fuzzyDate($timestamp, $full);
$fullDate = $this->formatToDateTime($timestamp, "T");
return '<abbr data-fuzzydate="' . $timestamp . '"></abbr>';
// return '<abbr data-toggle="tooltip" title="' . htmlentities($fullDate) . '">' . htmlentities($fuzzyDate) . '</abbr>';
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace Wruczek\TSWebsite\Utils\Language;
class Language {
private $languageId;
private $languageNameEnglish;
private $languageNameNative;
private $languageCode;
private $isDefault;
private $languageItems;
/**
* Language constructor.
* @param $languageId
* @param $languageNameEnglish
* @param $languageNameNative
* @param $languageCode
* @param $isDefault
* @param $languageItems
*/
public function __construct($languageId, $languageNameEnglish, $languageNameNative, $languageCode, $isDefault, $languageItems) {
$this->languageId = $languageId;
$this->languageNameEnglish = $languageNameEnglish;
$this->languageNameNative = $languageNameNative;
$this->languageCode = $languageCode;
$this->isDefault = $isDefault;
$this->languageItems = $languageItems;
}
/**
* Returns language ID
* @return int language ID
*/
public function getLanguageId() {
return $this->languageId;
}
/**
* Returns language name in English
* @return string language name in English
*/
public function getLanguageNameEnglish() {
return $this->languageNameEnglish;
}
/**
* Returns language name in its native form
* @return string language name in its native form
*/
public function getLanguageNameNative() {
return $this->languageNameNative;
}
/**
* Returns language code
* @return string language code
*/
public function getLanguageCode() {
return $this->languageCode;
}
/**
* Returns true when this language is set as default site language
* @return boolean true when default, false otherwise
*/
public function isDefault() {
return $this->isDefault;
}
/**
* Sets this language as default language of the site
* @return boolean true on success, false otherwise
*/
public function setAsDefaultLanguage() {
return LanguageUtils::i()->setDefaultLanguage($this);
}
/**
* Returns simple array with identifier -> value mapping, created from getLanguageItems()
* @return array
*/
public function getSimpleItemsArray() {
$ret = [];
foreach ($this->getLanguageItems() as $item) {
$ret[$item->getIdentifier()] = $item->getValue();
}
return $ret;
}
/**
* Returns language item
* @param $identifier string identifier
* @return LanguageItem LanguageItem if found, null otherwise
*/
public function getLanguageItem($identifier) {
foreach ($this->getLanguageItems() as $item) {
if(strcasecmp($item->getIdentifier(), $identifier) === 0)
return $item;
}
return null;
}
/**
* Returns language strings
* @return array array filled with LanguageItem
*/
public function getLanguageItems() {
return $this->languageItems;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Wruczek\TSWebsite\Utils\Language;
class LanguageItem {
private $identifier;
private $value;
private $comment;
/**
* LanguageItem constructor.
* @param $identifier
* @param $value
* @param $comment
*/
public function __construct($identifier, $value, $comment) {
$this->identifier = $identifier;
$this->value = $value;
$this->comment = $comment;
}
/**
* Returns item identifier
* @return string
*/
public function getIdentifier() {
return $this->identifier;
}
/**
* Returns item value
* @return string
*/
public function getValue() {
return $this->value;
}
/**
* Returns item comment, can be null
* @return string
*/
public function getComment() {
return $this->comment;
}
public function __toString() {
return $this->getValue();
}
}

View File

@ -0,0 +1,201 @@
<?php
namespace Wruczek\TSWebsite\Utils\Language;
use function htmlspecialchars;
use Wruczek\PhpFileCache\PhpFileCache;
use Wruczek\TSWebsite\Utils\DatabaseUtils;
use Wruczek\TSWebsite\Utils\SingletonTait;
/**
* Class LanguageUtils
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class LanguageUtils {
use SingletonTait;
private $cache;
private $languages;
/**
* Short function for translate
*/
public static function tl($identifier, $args = []) {
return self::i()->translate($identifier, $args);
}
private function __construct() {
$this->cache = new PhpFileCache(__CACHE_DIR, "translations");
$this->languages = $this->cache->refreshIfExpired("languages", function () {
return $this->refreshLanguageCache(false);
}, 300);
}
/**
* Returns language by its ID
* @param $languageId int Language ID
* @return Language|boolean returns Language when found, false otherwise
*/
public function getLanguageById($languageId) {
foreach ($this->getLanguages() as $lang) {
if($lang->getLanguageId() === $languageId)
return $lang;
}
return false;
}
/**
* Returns language by its Language Code
* @param $languageCode string Language Code
* @return Language|boolean returns Language when found, false otherwise
*/
public function getLanguageByCode($languageCode) {
foreach ($this->getLanguages() as $lang) {
if(strcasecmp($lang->getLanguageCode(), $languageCode) === 0)
return $lang;
}
return false;
}
/**
* Returns all available languages
* @return array|mixed
*/
public function getLanguages() {
return $this->languages;
}
/**
* Returns default language
* @return Language default language
*/
public function getDefaultLanguage() {
foreach ($this->getLanguages() as $lang) {
if($lang->isDefault())
return $lang;
}
return null;
}
/**
* Sets language as default
* @param $language Language
* @return boolean true on success, false otherwise
*/
public function setDefaultLanguage($language) {
$db = DatabaseUtils::i()->getDb();
// set all languages as non-default, if this succeeds...
if($db->update("languages", ["isdefault" => 0])) {
// ...then set only this language to default
$success = $db->update("languages", ["isdefault" => 1], ["langid", $language->getLanguageId()]);
$this->refreshLanguageCache();
return $success;
}
return false;
}
/**
* Tried to determine user language and returns it
* @return Language user language if determined, null otherwise
*/
public function detectUserLanguage() {
if (isset($_COOKIE["tswebsite_language"])) { // check cookie
$langcode = $_COOKIE["tswebsite_language"];
} else if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { // check http headers
$langcode = substr($_SERVER["HTTP_ACCEPT_LANGUAGE"], 0, 2);
}
// if language with that code exists, return it
if(!empty($langcode) && ($lang = $this->getLanguageByCode($langcode)))
return $lang;
return null;
}
/**
* Refreshes language cache, loads and returns new data
* @param bool $updateCache true if the file cache should also be updated
* @return array
*/
public function refreshLanguageCache($updateCache = true) {
$db = DatabaseUtils::i()->getDb();
$data = $db->select("languages", ["langid", "englishname", "nativename", "langcode", "isdefault"]);
$langs = [];
foreach ($data as $lang) {
$langid = $lang["langid"];
$englishname = $lang["englishname"];
$nativename = $lang["nativename"];
$langcode = $lang["langcode"];
$isdefault = $lang["isdefault"];
$strings = $db->select("translations", ["identifier", "value", "comment"], ["langid" => $langid]);
$languageItems = [];
foreach ($strings as $str)
$languageItems[] = new LanguageItem($str["identifier"], $str["value"], $str["comment"]);
$langs[] = new Language($langid, $englishname, $nativename, $langcode, $isdefault, $languageItems);
}
$this->languages = $langs;
if($updateCache)
$this->cache->store("languages", $langs, 300);
return $langs;
}
/**
* Returns translated text. If identifier is not found in the current
* language, it tries to get it from the default language.
* User language is determined with getDefaultLanguage() function.
* @param $identifier string Translation identifier
* @param array $args Arguments that will replace placeholders
* @return string Translated text
* @throws \Exception When default site or user language cannot
* be found, and/or if $identifier is not found
*/
public function translate($identifier, $args = []) {
if (!is_array($args)) {
$args = [$args];
}
$defaultlang = $this->getDefaultLanguage();
$lang = $this->getLanguageById(@$_SESSION["userlanguageid"]);
if(!$lang && !$defaultlang)
throw new \Exception("Cannot get user or default language");
$item = $lang->getLanguageItem($identifier);
if(!$item)
$item = $defaultlang->getLanguageItem($identifier);
if(!$item)
throw new \Exception("Cannot get translation for $identifier");
$val = $item->getValue();
// Replace placeholders with values from $args
foreach ($args as $i => $iValue) {
// Prevent argument placeholder injection
$iValue = str_replace(["{", "}"], ["&#123;", "&#125;"], $iValue);
$val = str_ireplace('{' . $i . '}', $iValue, $val);
}
return $val;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Wruczek\TSWebsite\Utils;
trait SingletonTait {
/**
* Call this method to get singleton
* @return self
*/
public static function Instance() {
static $inst = null;
if ($inst === null)
$inst = new self();
return $inst;
}
/**
* A shorthand to get the singleton
* @return self
*/
public static function i() {
return self::Instance();
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use function mt_rand;
use function var_dump;
use Wruczek\TSWebsite\Config;
/**
* Class TeamSpeakUtils
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class TeamSpeakUtils {
use SingletonTait;
protected $configUtils;
protected $tsNodeHost;
protected $tsNodeServer;
protected $exceptionsList = [];
private function __construct() {
$this->configUtils = Config::i();
}
/**
* Returns TeamSpeak3_Node_Host object created using
* data from config database
* @return \TeamSpeak3_Node_Host
*/
public function getTSNodeHost() {
if($this->tsNodeHost === null) {
$hostname = $this->configUtils->getValue("query_hostname");
$queryport = $this->configUtils->getValue("query_port");
$username = $this->configUtils->getValue("query_username");
$password = $this->configUtils->getValue("query_password");
try {
$tsNodeHost = \TeamSpeak3::factory("serverquery://$hostname:$queryport/?timeout=3");
$tsNodeHost->login($username, $password);
$this->tsNodeHost = $tsNodeHost;
} catch (\Exception $e) {
$this->addExceptionToExceptionsList($e);
}
}
return $this->tsNodeHost;
}
/**
* Returns TeamSpeak3_Node_Server object created
* using getTSNodeHost() method.
* @return \TeamSpeak3_Node_Server
*/
public function getTSNodeServer() {
// Don't continue if TSNodeHost is NULL (not working / not initialised)
if($this->tsNodeServer === null && $this->getTSNodeHost()) {
$port = $this->configUtils->getValue("tsserver_port");
try {
$this->tsNodeServer = $this->getTSNodeHost()->serverGetByPort($port);
$newNickname = Config::get("query_nickname");
// if available, set the query nickname. add random numbers to the end, so
// the bot will work even with a user/query of the same nickname online
if (isset($newNickname)) {
// try 5 times to change the nickname if the previous is already in use
for($i = 0; $i < 5; $i++) {
try {
$this->tsNodeServer->selfUpdate(["client_nickname" => $newNickname]);
break; // success - we have set the nickname
} catch (\TeamSpeak3_Exception $e) {
// error nickname in use
if ($e->getCode() === 513) {
// add something random to the name and try again
$newNickname .= mt_rand(0, 9);
} else {
// if thats other error than nickname in use, re-throw it
throw $e;
}
}
}
}
} catch (\Exception $e) {
$this->addExceptionToExceptionsList($e);
}
}
return $this->tsNodeServer;
}
/**
* Tries to download file from the TS3 server. It might be an actual file,
* icon or avatar. Returns downloaded data. Might throw exceptions when filetransfer fails.
* @param $filename
* @param int $cid Channel Id (defaults to 0 - server)
* @param string $cpw Channel password (defaults to empty)
* @return mixed
* @throws \TeamSpeak3_Adapter_ServerQuery_Exception
*/
public function ftDownloadFile($filename, $cid = 0, $cpw = "") {
$dl = $this->getTSNodeServer()->transferInitDownload(mt_rand(0x0000, 0xFFFF), $cid, $filename, $cpw);
$host = (false !== strpos($dl["host"], ":") ? "[" . $dl["host"] . "]" : $dl["host"]);
$filetransfer = \TeamSpeak3::factory("filetransfer://$host:" . $dl["port"]);
return $filetransfer->download($dl["ftkey"], $dl["size"]);
}
/**
* Resets current connection, forces to reconnect to the TeamSpeak server
* next time you call getTSNodeHost or getTSNodeServer
*/
public function reset() {
$this->tsNodeHost = null;
$this->tsNodeServer = null;
}
/**
* Checks TeamSpeak server connection
* Warning: it will connect to the TeamSpeak server to check the status.
* Use it just before accessing the server, preferably after checking cache.
* @return bool true if TeamSpeak connection succeeded, false otherwise
*/
public function checkTSConnection() {
return $this->getTSNodeHost() !== null
&& $this->getTSNodeServer() !== null
&& empty($this->getExceptionsList());
}
/**
* Adds exception to the exceptions list
* @param \Exception $exception
*/
public function addExceptionToExceptionsList($exception) {
$this->exceptionsList[$exception->getCode()] = $exception;
}
/**
* Returns array filled with connection exceptions collected
* when calling getTSNodeServer(), getTSNodeServer() and other methods
* @return array Array filled with exceptions. Empty if no exceptions where thrown.
*/
public function getExceptionsList() {
return $this->exceptionsList;
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use Latte\Engine;
use Latte\Runtime\Html;
use Wruczek\TSWebsite\AdminStatus;
use Wruczek\TSWebsite\Config;
use Wruczek\TSWebsite\Utils\Language\LanguageUtils;
/**
* Class TemplateUtils
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class TemplateUtils {
use SingletonTait;
protected $latte;
private $oldestCache;
private function __construct() {
$this->latte = new Engine();
$this->getLatte()->setTempDirectory(__CACHE_DIR . "/templates");
// Add custom filters...
$this->getLatte()->addFilter("fuzzyDateAbbr", function ($s) {
return new Html('<span data-relativetime="fuzzydate" data-timestamp="' . $s . '">{cannot convert ' . $s . '}</span>');
});
$this->getLatte()->addFilter("fullDate", function ($s) {
return new Html('<span data-relativetime="fulldate" data-timestamp="' . $s . '">{cannot convert ' . $s . '}</span>');
});
$this->getLatte()->addFilter("translate", function ($s, ...$args) {
return new Html(__get($s, $args));
});
}
/**
* Returns latte object
* @return \Latte\Engine Latte object
*/
public function getLatte() {
return $this->latte;
}
/**
* Echoes rendered template
* @see renderTemplateToString
*/
public function renderTemplate($templateName, $data = [], $loadLangs = true) {
echo $this->renderTemplateToString($templateName, $data, $loadLangs);
}
/**
* Renders and outputs the error template
* @param string $errorcode Error code
* @param string $errorname Error title
* @param string $description Error description
*/
public function renderErrorTemplate($errorcode = "", $errorname = "", $description = "") {
$data = ["errorcode" => $errorcode, "errorname" => $errorname, "description" => $description];
$this->renderTemplate("errorpage", $data, false);
}
/**
* @param $templateName string Name of the template file, without path and extension
* @param $data array Data passed to the template
* @param bool $loadLangs true if the languages should be loaded (requires working database connection)
* @return string Rendered template
* @throws \Exception when we cannot get the CSRF token
*/
public function renderTemplateToString($templateName, $data = [], $loadLangs = true) {
$dbutils = DatabaseUtils::i();
if($loadLangs) {
$userlang = LanguageUtils::i()->getLanguageById($_SESSION["userlanguageid"]);
$data["languageList"] = LanguageUtils::i()->getLanguages();
$data["userLanguage"] = $userlang;
}
if ($timestamp = $this->getOldestCacheTimestamp())
$data["oldestTimestamp"] = $timestamp;
$data["tsExceptions"] = TeamSpeakUtils::i()->getExceptionsList();
if(@$dbutils->isInitialised())
$data["sqlCount"] = @$dbutils->getDb()->query("SHOW SESSION STATUS LIKE 'Questions'")->fetch()["Value"];
else
$data["sqlCount"] = "none";
$data["config"] = Config::i()->getConfig();
$csrfToken = CsrfUtils::getToken();
$data["csrfToken"] = $csrfToken;
$data["csrfField"] = new Html('<input type="hidden" name="csrf-token" value="' . $csrfToken . '">');
if (Config::get("adminstatus_enabled")) {
$data["adminStatus"] = AdminStatus::i()->getStatus(
Config::get("adminstatus_groups"),
Config::get("adminstatus_mode"),
Config::get("adminstatus_hideoffline"),
Config::get("adminstatus_ignoredusers")
);
}
return $this->getLatte()->renderToString(__TEMPLATES_DIR . "/$templateName.latte", $data);
}
/**
* Returns time elapsed from website load start until now
* @param bool $raw If true, returns elapsed time in
* milliseconds. Defaults to false.
* @return string
*/
public static function getRenderTime($raw = false) {
if($raw) {
return microtime(true) - __RENDER_START;
} else {
return number_format(self::getRenderTime(true), 5);
}
}
/**
* Stores information about the oldest cached page element
* for later to be displayed in a warning
* @see getOldestCacheTimestamp
* @param $data
*/
public function storeOldestCache($data) {
if ($data["expired"] && (!$this->oldestCache || $this->oldestCache > $data["time"]))
$this->oldestCache = $data["time"];
}
/**
* @see storeOldestCache
* @return int Oldest cache timestamp, null if not set
*/
public function getOldestCacheTimestamp() {
return $this->oldestCache;
}
/**
* Outputs either script or link with all parameters needed
* @param $resourceType string must be either "stylesheet" or "script"
* @param $url string Relative or absolute path to the resource. {cdnjs} will be
* replaced with "https://cdnjs.cloudflare.com/ajax/libs"
* @param $parameter string|bool|null If boolean, its gonna treat it as a local
* resource and add a version timestamp. If string, its gonna treat it as a
* integrity hash and add it along with crossorigin="anonymous" tag.
*/
public static function includeResource($resourceType, $url, $parameter = null) {
$url = str_replace('{cdnjs}', 'https://cdnjs.cloudflare.com/ajax/libs', $url);
$attributes = "";
if (is_bool($parameter)) {
$filemtime = @filemtime(__BASE_DIR . "/" . $url);
if ($filemtime !== false) {
$url .= "?v=$filemtime";
}
} else if (is_string($parameter)) {
// NEEDS to start with a space!
$attributes = ' integrity="' . htmlspecialchars($parameter) . '" crossorigin="anonymous"';
}
if ($resourceType === "stylesheet") {
echo '<link rel="stylesheet" href="' . htmlspecialchars($url) . '"' . $attributes . '>';
} else if ($resourceType === "script") {
echo '<script src="' . htmlspecialchars($url) . '"' . $attributes . '></script>';
} else {
throw new \InvalidArgumentException("$resourceType is not a valid resource type");
}
}
/**
* @see includeResource
*/
public static function includeStylesheet($url, $parameter = null) {
self::includeResource("stylesheet", $url, $parameter);
}
/**
* @see includeResource
*/
public static function includeScript($url, $parameter = null) {
self::includeResource("script", $url, $parameter);
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace Wruczek\TSWebsite\Utils;
use Wruczek\TSWebsite\Config;
use Wruczek\TSWebsite\News\DefaultNewsStore;
use Wruczek\TSWebsite\News\INewsStore;
/**
* Class Utils
* @package Wruczek\TSWebsite\Utils
* @author Wruczek 2017
*/
class Utils {
private function __construct() {}
/**
* Strips the first line from string
* https://stackoverflow.com/a/7740485
* @param $str
* @return bool|string stripped text without the first line or false on failure
*/
public static function stripFirstLine($str) {
$position = strpos($str, "\n");
if($position === false)
return $str;
return substr($str, $position + 1);
}
/**
* Checks if $haystack starts with $needle
* https://stackoverflow.com/a/860509
* @param $haystack string
* @param $needle string
* @param bool $case set to false for case-insensitivity (default true)
* @return bool true if $haystack starts with $needle, false otherwise
*/
public static function startsWith($haystack, $needle, $case = true) {
if ($case)
return strpos($haystack, $needle, 0) === 0;
return stripos($haystack, $needle, 0) === 0;
}
/**
* Checks if $haystack ends with $needle
* https://stackoverflow.com/a/860509
* @param $haystack string
* @param $needle string
* @param bool $case set to false for case-insensitivity (default true)
* @return bool true if $haystack ends with $needle, false otherwise
*/
public static function endsWith($haystack, $needle, $case = true) {
$expectedPosition = strlen($haystack) - strlen($needle);
if ($case)
return strrpos($haystack, $needle, 0) === $expectedPosition;
return strripos($haystack, $needle, 0) === $expectedPosition;
}
/**
* Returns IP address with last two octets replaced with "***"
* @param $ip string IP to censor
* @return bool|string Censored IP on success, false on failure
* @throws \Exception When the IP address is invalid
*/
public static function censorIpAddress($ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = explode(".", $ip);
if (count($ip) >= 2) {
return "{$ip[0]}.{$ip[1]}.***.***";
}
return "(IPv4)";
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = explode(":", $ip);
if (count($ip) >= 2) {
return "{$ip[0]}:{$ip[1]}:***:***";
}
return "(IPv6)";
}
throw new \Exception("Invalid IP address $ip");
}
/**
* Returns client IP from REMOTE_ADDR or from HTTP_CF_CONNECTING_IP if using CF IP
* @param bool $useCfip if true, check and use HTTP_CF_CONNECTING_IP header if present.
* Falls back to REMOTE_ADDR if empty
* @return string IP address
*/
public static function getClientIp($useCfip = null) {
if ($useCfip === null) {
$useCfip = (bool) Config::get("usingcloudflare");
}
// If IPv6 localhost, return IPv4 localhost
if ($_SERVER["REMOTE_ADDR"] === "::1") {
return "127.0.0.1";
}
if (!empty($_SERVER["HTTP_CF_CONNECTING_IP"]) && $useCfip) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
return $_SERVER["REMOTE_ADDR"];
}
/**
* Returns currently used news store
* @return INewsStore|null
*/
public static function getNewsStore() {
$newsStore = null;
// if the current implementation is default
if (true) {
$newsStore = new DefaultNewsStore();
}
return $newsStore;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Wruczek\TSWebsite\Utils;
class ValidationUtils {
}

View File

@ -0,0 +1,359 @@
<?php
namespace Wruczek\TSWebsite;
use function __get;
use TeamSpeak3;
class ViewerRenderer {
private $imgPath;
private $resultHtml;
private $serverInfo;
private $channelList;
private $clientList;
private $serverGroupList;
private $channelGroupList;
private $renderQueryClients = false;
private $hiddenChannels = [];
public function __construct($imgPath) {
$this->imgPath = $imgPath;
$cm = CacheManager::i();
$this->serverInfo = $cm->getServerInfo();
$this->channelList = $cm->getChannelList();
$this->clientList = $cm->getClientList();
$this->serverGroupList = $cm->getServerGroupList();
$this->channelGroupList = $cm->getChannelGroupList();
}
/**
* Checks if we have successfully loaded all required data from cache.
* Loading data from CacheManager might fail for example when the server is offline,
* or when we dont have required permissions to check for a specific item.
* @return bool true on success, false otherwise
*/
public function checkRequiredData() {
return isset($this->channelList, $this->clientList, $this->serverGroupList, $this->channelGroupList);
}
private function add($html, ...$args) {
foreach ($args as $i => $iValue) {
// Prevent argument placeholder injection
$iValue = str_replace(["{", "}"], ["&#123;", "&#125;"], $iValue);
$html = str_ireplace('{' . $i . '}', $iValue, $html);
}
$this->resultHtml .= $html;
}
public function renderViewer() {
if (!$this->checkRequiredData()) {
throw new \Exception("Failed to load required data from the cache. " .
"Is the server online? Do we have enough permissions?");
}
$suffixIcons = "";
if ($icon = $this->serverInfo["virtualserver_icon_id"]) {
$suffixIcons = $this->getIcon($icon, __get("VIEWER_SERVER_ICON"));
}
$html = <<<EOD
<div class="channel-container is-server">
<div class="channel" data-channelid="0" tabindex="0">
<span class="channel-name">{0}{1}</span>
<span class="channel-icons">{2}</span>
</div>
</div>
EOD;
$this->add(
$html,
$this->getIcon("server_green.svg"),
htmlspecialchars($this->serverInfo["virtualserver_name"]),
$suffixIcons
);
foreach ($this->channelList as $channel) {
// Start rendering the top channels, they are gonna
// render all the childrens recursively
if ($channel["pid"] === 0) {
$this->renderChannel(new TeamSpeakChannel($channel));
}
}
return $this->resultHtml;
}
public function getIcon($name, $tooltip = null, $alt = "Icon") {
if (is_string($name)) {
$path = "{$this->imgPath}/$name";
} else {
$path = "api/geticon.php?iconid=" . (int) $name;
}
$ttip = $tooltip ? ' data-toggle="tooltip" title="' . htmlspecialchars($tooltip) . '"' : "";
return '<img class="icon" src="' . $path . '" alt="' . htmlspecialchars($alt) . '"' . $ttip . '>';
}
/**
* @param $channel TeamSpeakChannel
*/
public function renderChannel($channel) {
$hasParent = $channel->getParentId();
$isHidden = in_array($channel->getId(), $this->hiddenChannels);
$channelDisplayName = $channel->getDisplayName();
$channelClasses = $hasParent ? "has-parent" : "no-parent";
$channelIcon = "";
$suffixIcons = "";
// If this channel is occupied
if ($channel->isOccupied(false, $this->renderQueryClients) && !$isHidden) {
$channelClasses .= " is-occupied";
} else if ($channel->isOccupied(true, $this->renderQueryClients) && !$isHidden) {
$channelClasses .= " occupied-childs";
} else {
$channelClasses .= " not-occupied";
}
if ($channel->isSpacer()) {
$channelClasses .= " is-spacer";
switch($channel->getSpacerAlign()) {
case TeamSpeak3::SPACER_ALIGN_REPEAT:
$channelClasses .= " spacer-repeat";
$channelDisplayName = str_repeat($channelDisplayName, 200);
break;
case TeamSpeak3::SPACER_ALIGN_CENTER:
$channelClasses .= " spacer-center";
break;
case TeamSpeak3::SPACER_ALIGN_RIGHT:
$channelClasses .= " spacer-right";
break;
case TeamSpeak3::SPACER_ALIGN_LEFT:
$channelClasses .= " spacer-left";
break;
}
} else {
$channelIcon = $this->getChannelIcon($channel, $isHidden);
$suffixIcons = $this->getChannelSuffixIcons($channel);
}
$html = <<<EOD
<div class="channel-container {0}">
<div class="channel" data-channelid="{1}"{2}>
<span class="channel-name">{3}{4}</span>
<span class="channel-icons">{5}</span>
</div>
EOD;
$this->add(
$html,
$channelClasses,
$channel->getId(),
$channel->isSpacer() ? "" : ' tabindex="0"',
$channelIcon,
htmlspecialchars($channelDisplayName),
$suffixIcons
);
if (!$isHidden) {
foreach ($channel->getChannelMembers($this->renderQueryClients) as $member) {
$this->renderClient($member);
}
}
foreach ($channel->getChildChannels() as $member) {
$this->renderChannel($member);
}
$this->add('</div>' . PHP_EOL . PHP_EOL);
}
public function renderClient($client) {
$isQuery = (bool) $client["client_type"];
$clientSGIDs = explode(",", $client["client_servergroups"]);
$clientServerGroups = [];
$beforeName = [];
$afterName = [];
if (isset($client["client_away_message"])) {
$afterName[] = "[{$client["client_away_message"]}]";
}
foreach ($this->serverGroupList as $servergroup) {
$groupid = $servergroup["sgid"];
if (in_array($groupid, $clientSGIDs)) {
$clientServerGroups[$groupid] = $servergroup;
if ($servergroup["namemode"] === TeamSpeak3::GROUP_NAMEMODE_BEFORE) {
$beforeName[] = "[{$servergroup["name"]}]";
}
if ($servergroup["namemode"] === TeamSpeak3::GROUP_NAMEMODE_BEHIND) {
$afterName[] = "[{$servergroup["name"]}]";
}
}
}
$clientIcon = $this->getClientIcon($client);
$suffixIcons = $this->getClientSuffixIcons($client, $clientServerGroups, 0);
$html = <<<EOD
<div class="client-container{0}" data-clientdbid="{1}" tabindex="0">
<span class="client-name">{2}{3}</span>
<span class="client-icons">{4}</span>
</div>
EOD;
$clientName = implode(" ", $beforeName); // prefix groups
$clientName .= " {$client["client_nickname"]} "; // nickname
$clientName .= implode(" ", $afterName); // suffix groups
$clientName = htmlspecialchars(trim($clientName)); // trim and sanitize
$this->add(
$html,
$isQuery ? " is-query" : "", $client["client_database_id"],
$clientIcon,
$clientName,
$suffixIcons
);
}
private function getChannelIcon(TeamSpeakChannel $channel, $isHidden) {
$subscribed = $isHidden ? "" : "_subscribed";
$unsub = $isHidden ? __get("VIEWER_CHANNEL_UNSUB1") : "";
if ($channel->isDefaultChannel()) {
return $this->getIcon("channel_default.svg", __get("VIEWER_DEFAULT_CHANNEL"));
}
if ($channel->isFullyOccupied()) {
return $this->getIcon("channel_red{$subscribed}.svg", __get("VIEWER_CHANNEL_OCCUPIED") . $unsub);
}
if ($channel->hasPassword()) {
return $this->getIcon("channel_yellow{$subscribed}.svg", __get("VIEWER_CHANNEL_PASSWORD") . $unsub);
}
return $this->getIcon("channel_green{$subscribed}.svg", $isHidden ? __get("VIEWER_CHANNEL_UNSUB2") : null);
}
private function getChannelSuffixIcons(TeamSpeakChannel $channel) {
$info = $channel->getInfo();
$html = "";
if($channel->isDefaultChannel()) {
$html .= $this->getIcon("default.svg", __get("VIEWER_DEFAULT_CHANNEL"));
}
if($info["channel_flag_password"]) {
$html .= $this->getIcon("channel_private.svg", __get("VIEWER_CHANNEL_PASSWORD"));
}
$codec = $info["channel_codec"];
if($codec === TeamSpeak3::CODEC_CELT_MONO || $codec === TeamSpeak3::CODEC_OPUS_MUSIC) {
$html .= $this->getIcon("music.svg", __get("VIEWER_CHANNEL_MUSIC_CODED"));
}
if($info["channel_needed_talk_power"]) {
$html .= $this->getIcon("moderated.svg", __get("VIEWER_CHANNEL_MODERATED"));
}
if($info["channel_icon_id"]) {
$html .= $this->getIcon($info["channel_icon_id"], __get("VIEWER_CHANNEL_ICON"));
}
return $html;
}
public function getClientIcon($client) {
if($client["client_type"]) {
return $this->getIcon("server_query.svg");
}
if($client["client_away"]) {
return $this->getIcon("away.svg", htmlspecialchars($client["client_away_message"]) ?: __get("VIEWER_CLIENT_AWAY"));
}
if(!$client["client_output_hardware"]) {
return $this->getIcon("hardware_output_muted.svg", __get("VIEWER_CLIENT_OUTPUT_DISABLED"));
}
if($client["client_output_muted"]) {
return $this->getIcon("output_muted.svg", __get("VIEWER_CLIENT_OUTPUT_MUTED"));
}
if(!$client["client_input_hardware"]) {
return $this->getIcon("hardware_input_muted.svg", __get("VIEWER_CLIENT_MIC_DISABLED"));
}
if($client["client_input_muted"]) {
return $this->getIcon("input_muted.svg", __get("VIEWER_CLIENT_MIC_MUTED"));
}
if($client["client_is_channel_commander"]) {
return $this->getIcon("player_commander_off.svg", __get("VIEWER_CLIENT_COMMANDER"));
}
return $this->getIcon("player_off.svg");
}
public function getClientSuffixIcons($client, $groups, $cntp) {
$html = "";
if($client["client_is_priority_speaker"]) {
$html .= $this->getIcon("microphone.svg", __get("VIEWER_CLIENT_PRIORITY_SPEAKER"));
}
if($client["client_is_channel_commander"]) {
$html .= $this->getIcon("channel_commander.svg", __get("VIEWER_CLIENT_COMMANDER"));
}
if($client["client_is_talker"]) {
$html .= $this->getIcon("talk_power_grant.svg", __get("VIEWER_CLIENT_TALK_POWER_GRANTED"));
} else if($cntp && $cntp > $client["client_talk_power"]) {
$html .= $this->getIcon("input_muted.svg", __get("VIEWER_CLIENT_TALK_POWER_INSUFFICIENT"));
}
foreach ($groups as $group) {
if ($group["iconid"]) {
$icon = $group["iconid"];
} else {
$icon = "broken_image.svg";
continue;
// If the group does not have an icon, we skip this group.
// However, you can comment out the above "continue" statement
// to show the group with a "broken-image" icons.
}
$html .= $this->getIcon($icon, htmlspecialchars($group["name"]));
}
if($client["client_icon_id"]) {
$html .= $this->getIcon($client["client_icon_id"], __get("VIEWER_CLIENT_ICON"));
}
if($client["client_country"]) {
$country = $client["client_country"];
$countryLower = strtolower($country);
$html .= '<i class="icon-flag famfamfam-flags ' . $countryLower . '" ' .
'data-toggle="tooltip" title="' . $country . '" aria-hidden="true"></i>';
}
return $html;
}
}

View File

@ -0,0 +1,28 @@
<?php
define("__TSWEBSITE_VERSION", "dev-2.0.0");
define("__BASE_DIR", __DIR__ . "/../..");
define("__PRIVATE_DIR", __BASE_DIR . "/private");
define("__CACHE_DIR", __PRIVATE_DIR . "/cache");
define("__TEMPLATES_DIR", __PRIVATE_DIR . "/templates");
define("__CONFIG_FILE", __PRIVATE_DIR . "/dbconfig.php");
define("__LOCALDB_FILE", __PRIVATE_DIR . "/.sqlite.db.php");
define("__INSTALLER_LOCK_FILE", __PRIVATE_DIR . "/INSTALLER_LOCK");
define("__DEV_MODE", defined("DEV_MODE") || getenv("DEV_MODE") || file_exists(__PRIVATE_DIR . "/dev_mode"));
// utf8_encode polyfill - function required by TS3PHPFramework
// Taken from: https://github.com/symfony/polyfill (MIT License)
if (!function_exists("utf8_encode")) {
define("__USING_U8ENC_POLYFILL", true);
function utf8_encode($s) {
$s .= $s;
$len = strlen($s);
for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
switch (true) {
case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
default: $s[$j] = "\xC3"; $s[++$j] = chr(ord($s[$i]) - 64); break;
}
}
return substr($s, 0, $j);
}
}

87
src/private/php/load.php Normal file
View File

@ -0,0 +1,87 @@
<?php
use Wruczek\TSWebsite\Config;
use Wruczek\TSWebsite\ServerIconCache;
use Wruczek\TSWebsite\Utils\CsrfUtils;
use Wruczek\TSWebsite\Utils\Language\LanguageUtils;
session_name("tswebsite_sessionid");
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
define("__RENDER_START", microtime(true));
require_once __DIR__ . "/constants.php";
@header("TSW_DevMode: " . (__DEV_MODE ? "enabled" : "disabled"));
if(__DEV_MODE) {
ini_set("display_errors", 1);
ini_set("display_startup_errors", 1);
error_reporting(E_ALL);
}
if(!file_exists(__INSTALLER_LOCK_FILE)) {
if(file_exists(__BASE_DIR . "/installer")) {
header("Location: installer/index.php");
} else {
echo '&#129300; Something is not right! Looks like the website is not installed ("private/INSTALLER_LOCK" not found or is empty), but ' .
'installation wizard folder "installer" cannot be found! Please start the installation again and follow installation guide step-by-step.';
}
exit;
}
require_once __PRIVATE_DIR . "/vendor/autoload.php";
// Check CSRF token if needed and validate it
if (!defined("DISABLE_CSRF_CHECK") &&
in_array($_SERVER["REQUEST_METHOD"], ["POST", "PUT", "DELETE", "PATCH"])
) {
CsrfUtils::validateRequest();
}
// Try to guess user language and store it
// If the current language is not defined, or is invalid then return to default
if(!isset($_SESSION["userlanguageid"])) {
$lang = LanguageUtils::i()->detectUserLanguage();
if(!$lang) {
$lang = LanguageUtils::i()->getDefaultLanguage();
}
$_SESSION["userlanguageid"] = $lang->getLanguageId();
}
// Shortcut to language functions
{
/**
* Shortcut to translate and output the result
*/
function __($identifier, $args = []) {
echo __get($identifier, $args);
}
/**
* Shortcut to translate and return the result
*/
function __get($identifier, $args = []) {
try {
return LanguageUtils::i()->translate($identifier, $args);
} catch (\Exception $e) {
return "(unknown translation for " . htmlspecialchars($identifier) . ")";
}
}
}
// Set timezone
date_default_timezone_set(Config::get("timezone"));
// Init TS3 library
// This makes it possible to cache TS3 library objects
TeamSpeak3::init();
// Sync server icon cache if needed
ServerIconCache::syncIfNeeded();