First release of 2.0! :D
This commit is contained in:
143
src/private/php/AdminStatus.php
Normal file
143
src/private/php/AdminStatus.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
136
src/private/php/Assigner.php
Normal file
136
src/private/php/Assigner.php
Normal 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
288
src/private/php/Auth.php
Normal 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 {}
|
160
src/private/php/CacheManager.php
Normal file
160
src/private/php/CacheManager.php
Normal 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
179
src/private/php/Config.php
Normal 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;
|
||||
}
|
||||
}
|
106
src/private/php/News/DefaultNewsStore.php
Normal file
106
src/private/php/News/DefaultNewsStore.php
Normal 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;
|
||||
}
|
||||
}
|
62
src/private/php/News/INewsStore.php
Normal file
62
src/private/php/News/INewsStore.php
Normal 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);
|
||||
|
||||
}
|
105
src/private/php/ServerIconCache.php
Normal file
105
src/private/php/ServerIconCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
232
src/private/php/TeamSpeakChannel.php
Normal file
232
src/private/php/TeamSpeakChannel.php
Normal 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();
|
||||
}
|
||||
}
|
108
src/private/php/Utils/ApiUtils.php
Normal file
108
src/private/php/Utils/ApiUtils.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
80
src/private/php/Utils/CsrfUtils.php
Normal file
80
src/private/php/Utils/CsrfUtils.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
51
src/private/php/Utils/DatabaseUtils.php
Normal file
51
src/private/php/Utils/DatabaseUtils.php
Normal 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);
|
||||
}
|
||||
}
|
117
src/private/php/Utils/DateUtils.php
Normal file
117
src/private/php/Utils/DateUtils.php
Normal 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>';
|
||||
}
|
||||
}
|
114
src/private/php/Utils/Language/Language.php
Normal file
114
src/private/php/Utils/Language/Language.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
50
src/private/php/Utils/Language/LanguageItem.php
Normal file
50
src/private/php/Utils/Language/LanguageItem.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
201
src/private/php/Utils/Language/LanguageUtils.php
Normal file
201
src/private/php/Utils/Language/LanguageUtils.php
Normal 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(["{", "}"], ["{", "}"], $iValue);
|
||||
|
||||
$val = str_ireplace('{' . $i . '}', $iValue, $val);
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
}
|
29
src/private/php/Utils/SingletonTait.php
Normal file
29
src/private/php/Utils/SingletonTait.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
148
src/private/php/Utils/TeamSpeakUtils.php
Normal file
148
src/private/php/Utils/TeamSpeakUtils.php
Normal 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;
|
||||
}
|
||||
}
|
193
src/private/php/Utils/TemplateUtils.php
Normal file
193
src/private/php/Utils/TemplateUtils.php
Normal 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);
|
||||
}
|
||||
}
|
132
src/private/php/Utils/Utils.php
Normal file
132
src/private/php/Utils/Utils.php
Normal 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;
|
||||
}
|
||||
}
|
9
src/private/php/Utils/ValidationUtils.php
Normal file
9
src/private/php/Utils/ValidationUtils.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Wruczek\TSWebsite\Utils;
|
||||
|
||||
class ValidationUtils {
|
||||
|
||||
|
||||
|
||||
}
|
359
src/private/php/ViewerRenderer.php
Normal file
359
src/private/php/ViewerRenderer.php
Normal 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(["{", "}"], ["{", "}"], $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;
|
||||
}
|
||||
}
|
28
src/private/php/constants.php
Normal file
28
src/private/php/constants.php
Normal 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
87
src/private/php/load.php
Normal 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 '🤔 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();
|
Reference in New Issue
Block a user