Archived
1
0
Fork 0

Modified channel files, removed about modal

This commit is contained in:
Varakh 2019-08-07 17:56:21 +02:00
parent cae20ac477
commit 3bb5e51b30
30 changed files with 292 additions and 103 deletions

View file

@ -1,5 +1,15 @@
# CHANGELOG # CHANGELOG
## 2.1.0 - 2019/08/07
* Fixed file handling on snapshots
* Cleaned up template links
* Updated documentation
* Added application log view
* Fixed files view
* Added show total used space for files in a channel
* Added file delete action
* Removed about modal
## 2.0.0 - 2019/08/06 ## 2.0.0 - 2019/08/06
* Replace material design with bootstrap4 theme * Replace material design with bootstrap4 theme
* Add an about modal * Add an about modal

View file

@ -1,28 +1,46 @@
# README # README
ts3web is a free and open-source web interface for TeamSpeak 3 instances.
ts3web is a web interface for one TeamSpeak 3 Server. It's using serverquery to login. The minimalistic approach of this application is intentional.
This web interface aims to be as simple as possible. The minimalistic approach is intentional. * Docker images available on https://hub.docker.com/r/varakh/ts3web
* Sources are hosted on https://git.myservermanager.com/alexander.schaeferdiek/ts3web
Feel free to submit pull requests if you like to help. More information are here: [https://hub.docker.com/r/varakh/ts3web](https://hub.docker.com/r/varakh/ts3web) ## Limitations
Features which are currently not supported:
Features which are currently **not supported**:
* upload files (only viewing and deleting)
* modify permissions (only viewing) * modify permissions (only viewing)
* modify files (only viewing)
**ts3web** can be deployed in different ways. See below for more information. For each deployment type a running ## F.A.Q
TeamSpeak 3 server is a prerequisite (except for the `docker-compose.yml` type which will start also the server if
needed). ###### There are lots of TeamSpeak 3 web interfaces out. Why should I pick ts3web?
Free, simple, stateless, easy to extend, standard bootstrap theme.
###### I always get `TSException: Error: host isn't a ts3 instance!` when selecting a server.
You probably got query banned from your server. You need to properly define your `whitelist.txt` file and include it in
your TeamSpeak application.
## Configuration ## Configuration
The main configuration file is the `env` file located in `config/`. There's an example file called `env.example` The main configuration file is the `env` file located in `config/`. There's an example file called `env.example`
which you can copy to `config/env`. Defaults will assume you're running your TeamSpeak server on `localhost` with which you **need* to copy to `config/env`. Defaults will assume you're running your TeamSpeak server on `localhost` with
default port. Docker deployments can host bind this file into the container directly and just maintain the `env` file. default port. Docker deployments can host bind this file into the container directly and just maintain the `env` file.
## Usage with docker-compose ## Deployment
The application can be deployed in different ways. See below for more information. For each deployment type a running
TeamSpeak 3 instance is a prerequisite (except for the `docker-compose.yml` type which will start also the server if
needed).
### Exposed volumes on docker images
* Snapshots are saved in `/var/www/html/application/data/snapshots`. You should create a volume for this location if
you're using docker as deployment type.
* Logs are saved in `/var/www/html/application/log` for docker containers. You should create a volume
for this location if you're using docker as deployment type.
**Important**: Ensure that host binds have permissions set up properly. The user which is used in the docker container is `www-data` with
id `82`. If, e.g. logs are host bound, then execute `chown -R 82:82 host/path/to/log`. The same holds true for snapshots.
### Usage with docker-compose
The recommended way is to use docker-compose. The `network_mode = "host"` is required in order to show correct IP The recommended way is to use docker-compose. The `network_mode = "host"` is required in order to show correct IP
addresses of connected users. addresses of connected users.
@ -59,6 +77,7 @@ services:
volumes: volumes:
- ./env:/var/www/html/application/config/env - ./env:/var/www/html/application/config/env
- ./snapshots:/var/www/html/application/data/snapshots - ./snapshots:/var/www/html/application/data/snapshots
- ./log:/var/www/html/application/log
ports: ports:
- 127.0.0.1:8181:80 - 127.0.0.1:8181:80
depends_on: depends_on:
@ -76,16 +95,13 @@ Your TeamSpeak 3 Server will be available under `public-server-ip:9987`. The web
For testing purposes, change `- 127.0.0.1:8181:80` to `- 8181:80`. The web interface will then be available under For testing purposes, change `- 127.0.0.1:8181:80` to `- 8181:80`. The web interface will then be available under
`public-server-ip:8181`. This is **not recommended**! Secure your setup properly via reverse proxy and SSL. `public-server-ip:8181`. This is **not recommended**! Secure your setup properly via reverse proxy and SSL.
Snapshots are saved in `/var/www/html/application/data/snapshots`. You should create a volume for this location. ### Usage as single docker container
## Usage as single docker container
* Copy `env.example` to `env` and adjust to your needs. It's recommended to make it persistent outside of the container. * Copy `env.example` to `env` and adjust to your needs. It's recommended to make it persistent outside of the container.
* Create a container with the image, e.g. `docker run --name teamspeak_web -v ./env:/var/www/html/application/config/env -p 8181:80 varakh/ts3web:latest`. * Create a container with the image, e.g. `docker run --name teamspeak_web -v ./env:/var/www/html/application/config/env -p 8181:80 varakh/ts3web:latest`.
* Make sure that if teamspeak and ts3web share the same docker instance they should be put into one network and the subnet **needs be added to teamspeak's query whitelist**. * Make sure that if teamspeak and ts3web share the same docker instance they should be put into one network and the subnet **needs be added to teamspeak's query whitelist**.
* Point your browser to `8181` to see the web interface. * Point your browser to `8181` to see the web interface.
## Usage as native application ### Usage as native application
**Prerequisite**: `php`, `composer` and probably `php-fpm` installed on the server. **Prerequisite**: `php`, `composer` and probably `php-fpm` installed on the server.
To install: To install:
@ -100,7 +116,7 @@ To upgrade:
* `git pull` * `git pull`
* `composer update` * `composer update`
## Web server setup ### Web server setup
* Example `nginx.conf` for **standalone** deployment without SSL: * Example `nginx.conf` for **standalone** deployment without SSL:
``` ```
@ -153,7 +169,6 @@ To upgrade:
* Tag the release git commit and create a new release in the VCS web interface * Tag the release git commit and create a new release in the VCS web interface
### Helpers ### Helpers
Attributes can be defined when including `table`, `keyvalues` and `form` templates of twig. This helps to generate tables and forms without the need to specify all attributes. Attributes can be defined when including `table`, `keyvalues` and `form` templates of twig. This helps to generate tables and forms without the need to specify all attributes.
``` ```

View file

@ -70,6 +70,7 @@ class ACL extends \Zend\Permissions\Acl\Acl
'/channels/edit/{sid}/{cid}', '/channels/edit/{sid}/{cid}',
'/channels/delete/{sid}/{cid}', '/channels/delete/{sid}/{cid}',
'/channels/send/{sid}/{cid}', '/channels/send/{sid}/{cid}',
'/channels/files/delete/{sid}/{cid}',
'/groups/{sid}', '/groups/{sid}',

View file

@ -50,6 +50,11 @@ class EnvConstants
*/ */
const TEAMSPEAK_USER = "teamspeak_user"; const TEAMSPEAK_USER = "teamspeak_user";
/**
* TeamSpeak log lines
*/
const TEAMSPEAK_LOG_LINES = "teamspeak_log_lines";
/** /**
* Log name * Log name
*/ */

View file

@ -12,6 +12,7 @@ teamspeak_host="localhost" # 'localhost' or 'name_of_docker_container' if runnin
teamspeak_query_port=10011 teamspeak_query_port=10011
teamspeak_user="serveradmin" teamspeak_user="serveradmin"
teamspeak_tree_view="true" # show a tree view in the details of online clients if a server has been selected teamspeak_tree_view="true" # show a tree view in the details of online clients if a server has been selected
teamspeak_log_lines=100 # show this amount of latest log lines
# log # log
log_name="ts3web" # values: all strings log_name="ts3web" # values: all strings

View file

@ -294,6 +294,11 @@ $container[ChannelSendAction::class] = function ($container) {
}; };
$app->post('/channels/send/{sid}/{cid}', ChannelSendAction::class); $app->post('/channels/send/{sid}/{cid}', ChannelSendAction::class);
$container[ChannelFilesDeleteAction::class] = function ($container) {
return new ChannelFilesDeleteAction($container);
};
$app->get('/channels/files/delete/{sid}/{cid}', ChannelFilesDeleteAction::class);
// tokens // tokens
$container[TokensAction::class] = function ($container) { $container[TokensAction::class] = function ($container) {
return new TokensAction($container); return new TokensAction($container);

View file

@ -58,7 +58,7 @@ validate_valid_url: "The %field% field is required to be a valid URL."
# menu # menu
menu.instance: "Instance" menu.instance: "Instance"
menu.servers: "Servers" menu.servers: "Servers"
menu.logs: "Instance Log" menu.logs: "Logs"
menu.profile: "Profile" menu.profile: "Profile"
menu.servers.info: "Info" menu.servers.info: "Info"
@ -76,7 +76,6 @@ menu.servers.logs: "Log"
# titles # titles
instance.title: "Instance" instance.title: "Instance"
servers.title: "Servers" servers.title: "Servers"
logs.title: "Latest 100 Log Entries"
server_info.title: "Server Info" server_info.title: "Server Info"
online.title: "Online Clients" online.title: "Online Clients"
online_info.title: "Online Info" online_info.title: "Online Info"
@ -93,6 +92,9 @@ profile.title: "Profile"
tokens.title: "Tokens" tokens.title: "Tokens"
snapshots.title: "Snapshots" snapshots.title: "Snapshots"
passwords.title: "Passwords" passwords.title: "Passwords"
instance_logs.title: "Instance log: latest 100 entries"
server_logs.title: "Server log: latest 100 entries"
app_log.title: "Application log"
# dynamic render of key value pairs # dynamic render of key value pairs
key: "Attribute" key: "Attribute"
@ -157,16 +159,23 @@ channels.create.parent: "Parent"
channel_info.h.files: "Files" channel_info.h.files: "Files"
channel_info.h.actions: "Actions" channel_info.h.actions: "Actions"
channel_info.h.details: "Details" channel_info.h.details: "Details"
channel_info.h.clients: "Clients" channel_info.h.clients: "Current clients"
channel_info.send: "Send a message" channel_info.send: "Send a message"
channel_info.send.message: "Message" channel_info.send.message: "Message"
channel_info.client: "Client" channel_info.client: "Client"
channel_info.files.delete: "Delete" channel_info.files.delete: "Delete"
channel_info.files.h.path: "Path"
channel_info.files.h.type: "Type"
channel_info.files.h.size: "Size"
channel_info.files.h.datetime: "Datetime"
channel_info.files.h.delete: "Delete"
channel_info.files.delete.success: "Deleted %file%."
# groups # groups
groups.delete: "Delete" groups.delete: "Delete"
groups.h.servergroups: "Server Groups" groups.h.servergroups: "Server Groups"
groups.h.channelgroups: "Channel Groups" groups.h.channelgroups: "Channel Groups"
groups.servergroup: "Server Group"
groups.channelgroup: "Channel Group"
# groups create/copy # groups create/copy
groups.create: "Create or copy" groups.create: "Create or copy"
@ -262,6 +271,9 @@ snapshots.create: "Create a new snapshot"
snapshots.h.details: "Details" snapshots.h.details: "Details"
snapshots.deploy: "Deploy" snapshots.deploy: "Deploy"
snapshots.delete: "Delete" snapshots.delete: "Delete"
snapshots.error.create: "An error occurred when creating a snapshot. Please view the application log."
snapshots.error.delete: "An error occurred when deleting a snapshot. Please view the application log."
snapshots.error.deploy: "An error occurred when deploying a snapshot. Please view the application log."
# passwords # passwords
passwords.h.actions: "Actions" passwords.h.actions: "Actions"
@ -272,10 +284,4 @@ passwords.add.duration: "Duration (in seconds)"
passwords.add.description: "Description" passwords.add.description: "Description"
passwords.add.channel: "Channel (user joins)" passwords.add.channel: "Channel (user joins)"
passwords.add.channel_password: "Channelpassword" passwords.add.channel_password: "Channelpassword"
passwords.channel: "Channel"
# about
about.header: "About %name%"
about.body: |
ts3web is a web interface for one TeamSpeak 3 Server. It's using serverquery to login. This web interface aims to be as simple as possible. The minimalistic approach is intentional.
Feel free to submit pull requests if you like to help. More information are here: https://hub.docker.com/r/varakh/ts3web

View file

@ -19,7 +19,8 @@ RUN mkdir -p /var/www/html/application/bin/ \
&& chmod -R 777 /var/www/html/application/data/ \ && chmod -R 777 /var/www/html/application/data/ \
&& mkdir -p /var/www/html/application/log/ \ && mkdir -p /var/www/html/application/log/ \
&& touch /var/www/html/application/log/application.log \ && touch /var/www/html/application/log/application.log \
&& chmod 777 /var/www/html/application/log/application.log && chmod 777 /var/www/html/application/log/application.log \
&& chown -R www-data:www-data /var/www/html/application
# initialize app # initialize app
RUN cd /var/www/html/application/ \ RUN cd /var/www/html/application/ \

View file

@ -10,8 +10,13 @@ error_reporting(E_ALL);
*/ */
use Carbon\Carbon; use Carbon\Carbon;
use JeremyKendall\Slim\Auth\ServiceProvider\SlimAuthProvider;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
use Slim\Middleware\Session;
use Slim\Views\Twig;
use Slim\Views\TwigExtension;
use SlimSession\Helper;
// To help the built-in PHP dev server, check if the request was actually for // To help the built-in PHP dev server, check if the request was actually for
// something which should probably be served as a static file // something which should probably be served as a static file
@ -77,17 +82,17 @@ $container['authAdapter'] = function ($container) {
$container['acl'] = function () { $container['acl'] = function () {
return new ACL(); return new ACL();
}; };
$container->register(new \JeremyKendall\Slim\Auth\ServiceProvider\SlimAuthProvider()); $container->register(new SlimAuthProvider());
$app->add($app->getContainer()->get('slimAuthRedirectMiddleware')); $app->add($app->getContainer()->get('slimAuthRedirectMiddleware'));
// session // session
$app->add(new \Slim\Middleware\Session([ $app->add(new Session([
'name' => 'dummy_session', 'name' => 'dummy_session',
'autorefresh' => true, 'autorefresh' => true,
'lifetime' => '1 hour' 'lifetime' => '1 hour'
])); ]));
$container['session'] = function () { $container['session'] = function () {
return new \SlimSession\Helper; return new Helper;
}; };
// view // view
@ -103,8 +108,8 @@ $container['view'] = function ($container) use ($app) {
$themeCacheDir = false; $themeCacheDir = false;
} }
$view = new \Slim\Views\Twig($themeDir, ['cache' => $themeCacheDir]); $view = new Twig($themeDir, ['cache' => $themeCacheDir]);
$view->addExtension(new \Slim\Views\TwigExtension( $view->addExtension(new TwigExtension(
$container['router'], $container['router'],
$container['request']->getUri() $container['request']->getUri()
)); ));

View file

@ -0,0 +1,27 @@
<?php
use Slim\Http\Request;
use Slim\Http\Response;
final class ChannelFilesDeleteAction extends AbstractAction
{
public function __invoke(Request $request, Response $response, $args)
{
$sid = $args['sid'];
$cid = $args['cid'];
$file = null;
if (array_key_exists('file', $request->getQueryParams())) {
$file = urldecode($request->getQueryParams()['file']);
}
$this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']);
$this->ts->getInstance()->selectServer($sid, 'serverId');
$files = [$file];
$this->ts->getInstance()->ftDeleteFile($cid, '', $files);
$this->flash->addMessage('success', $this->translator->trans('channel_info.files.delete.success', ['%file%' => $file]));
return $response->withRedirect('/channels/' . $sid . '/' . $cid);
}
}

View file

@ -45,12 +45,12 @@ final class ChannelInfoAction extends AbstractAction
if (!empty($foundFiles)) { if (!empty($foundFiles)) {
foreach ($foundFiles as $file) { foreach ($foundFiles as $file) {
if ($file['type'] !== "0") { if ($file['type'] !== "0") { // a file
$file['path'] = $path; $file['path'] = $path;
$files[] = $file; $files[] = $file;
} }
if ($file['type'] === "0") { if ($file['type'] === "0") { // a directory
if ($path === '/') { if ($path === '/') {
$newPath = $path . $file['name']; $newPath = $path . $file['name'];
@ -58,6 +58,9 @@ final class ChannelInfoAction extends AbstractAction
$newPath = $path . '/' . $file['name']; $newPath = $path . '/' . $file['name'];
} }
$file['path'] = $path;
$files[] = $file;
$files = $this->getAllFilesIn($sid, $cid, $newPath, $files); $files = $this->getAllFilesIn($sid, $cid, $newPath, $files);
} }
} }

View file

@ -11,17 +11,21 @@ final class LogsAction extends AbstractAction
if (array_key_exists('sid', $args)) $sid = $args['sid']; if (array_key_exists('sid', $args)) $sid = $args['sid'];
$this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); $this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']);
$appLog = [];
if (empty($sid)) { if (empty($sid)) {
$dataResult = $this->ts->getInstance()->logView(100, 1, 1); $dataResult = $this->ts->getInstance()->logView(getenv(EnvConstants::TEAMSPEAK_LOG_LINES), 1, 1);
$appLog = explode("\n", file_get_contents(BootstrapHelper::getLogFile()));
} else { } else {
$selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId');
$dataResult = $this->ts->getInstance()->logView(100, 1, 0); $dataResult = $this->ts->getInstance()->logView(getenv(EnvConstants::TEAMSPEAK_LOG_LINES), 1, 0);
} }
// render GET // render GET
$this->view->render($response, 'logs.twig', [ $this->view->render($response, 'logs.twig', [
'title' => $this->translator->trans('logs.title'), 'title' => empty($sid) ? $this->translator->trans('instance_logs.title') : $this->translator->trans('server_logs.title'),
'data' => $this->ts->getInstance()->getElement('data', $dataResult), 'log' => $this->ts->getInstance()->getElement('data', $dataResult),
'appLog' => $appLog,
]); ]);
} }
} }

View file

@ -3,6 +3,7 @@
use Carbon\Carbon; use Carbon\Carbon;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
final class SnapshotCreateAction extends AbstractAction final class SnapshotCreateAction extends AbstractAction
@ -23,8 +24,13 @@ final class SnapshotCreateAction extends AbstractAction
if ($fileSystem->exists($path)) { if ($fileSystem->exists($path)) {
$this->flash->addMessage('error', $this->translator->trans('file.exists')); $this->flash->addMessage('error', $this->translator->trans('file.exists'));
} else { } else {
try {
$fileSystem->appendToFile($path, trim($snapshotCreateResult['data'])); $fileSystem->appendToFile($path, trim($snapshotCreateResult['data']));
$this->flash->addMessage('success', $this->translator->trans('done')); $this->flash->addMessage('success', $this->translator->trans('done'));
} catch (IOException $e) {
$this->logger->error('Could not write to ' . $path . '. Cause: ' . $e->getMessage());
$this->flash->addMessage('error', $this->translator->trans('snapshots.error.create'));
}
} }
return $response->withRedirect('/snapshots/' . $sid); return $response->withRedirect('/snapshots/' . $sid);

View file

@ -3,6 +3,7 @@
use Carbon\Carbon; use Carbon\Carbon;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
final class SnapshotDeleteAction extends AbstractAction final class SnapshotDeleteAction extends AbstractAction
@ -21,8 +22,8 @@ final class SnapshotDeleteAction extends AbstractAction
if (!$fileSystem->exists($path)) { if (!$fileSystem->exists($path)) {
$this->flash->addMessage('error', $this->translator->trans('file.notexists')); $this->flash->addMessage('error', $this->translator->trans('file.notexists'));
} else { } else {
try {
$fileSystem->remove($path); $fileSystem->remove($path);
$serverPath = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid; $serverPath = FileHelper::SNAPSHOTS_PATH . DIRECTORY_SEPARATOR . $sid;
if (count(FileHelper::getFiles($serverPath)) == 0) { if (count(FileHelper::getFiles($serverPath)) == 0) {
@ -30,6 +31,10 @@ final class SnapshotDeleteAction extends AbstractAction
} }
$this->flash->addMessage('success', $this->translator->trans('done')); $this->flash->addMessage('success', $this->translator->trans('done'));
} catch (IOException $e) {
$this->logger->error('Could not delete ' . $path . '. Cause: ' . $e->getMessage());
$this->flash->addMessage('error', $this->translator->trans('snapshots.error.delete'));
}
} }
return $response->withRedirect('/snapshots/' . $sid); return $response->withRedirect('/snapshots/' . $sid);

View file

@ -1,6 +1,5 @@
<?php <?php
use Carbon\Carbon;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
@ -21,9 +20,14 @@ final class SnapshotDeployAction extends AbstractAction
if (!$fileSystem->exists($path)) { if (!$fileSystem->exists($path)) {
$this->flash->addMessage('error', $this->translator->trans('file.notexists')); $this->flash->addMessage('error', $this->translator->trans('file.notexists'));
} else { } else {
try {
$snapshotData = file_get_contents($path); $snapshotData = file_get_contents($path);
$this->ts->getInstance()->serverSnapshotDeploy($snapshotData, true); $this->ts->getInstance()->serverSnapshotDeploy($snapshotData, true);
$this->flash->addMessage('success', $this->translator->trans('done')); $this->flash->addMessage('success', $this->translator->trans('done'));
} catch (Exception $e) {
$this->logger->error('Could not deploy ' . $path . '. Cause: ' . $e->getMessage());
$this->flash->addMessage('error', $this->translator->trans('snapshots.error.deploy'));
}
} }
return $response->withRedirect('/snapshots/' . $sid); return $response->withRedirect('/snapshots/' . $sid);

View file

@ -45,7 +45,7 @@ class TSAuthAdapter extends \Zend\Authentication\Adapter\AbstractAdapter
$password = $this->getCredential(); $password = $this->getCredential();
if ($this->ts->login($user, $password)) { if ($this->ts->login($user, $password)) {
$this->logger->info(sprintf('Authenticated as %s', $user)); $this->logger->debug(sprintf('Authenticated as %s', $user));
$user = ['identity' => $user, 'user' => $user, 'password'=> $password, 'role' => ACL::ACL_DEFAULT_ROLE_ADMIN]; $user = ['identity' => $user, 'user' => $user, 'password'=> $password, 'role' => ACL::ACL_DEFAULT_ROLE_ADMIN];
return new Result(Result::SUCCESS, $user, array()); return new Result(Result::SUCCESS, $user, array());

View file

@ -5,6 +5,8 @@ use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use Monolog\Processor\UidProcessor; use Monolog\Processor\UidProcessor;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Translation\Loader\YamlFileLoader; use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Translator;
@ -34,6 +36,7 @@ class BootstrapHelper
EnvConstants::TEAMSPEAK_HOST, EnvConstants::TEAMSPEAK_HOST,
EnvConstants::TEAMSPEAK_QUERY_PORT, EnvConstants::TEAMSPEAK_QUERY_PORT,
EnvConstants::TEAMSPEAK_USER, EnvConstants::TEAMSPEAK_USER,
EnvConstants::TEAMSPEAK_LOG_LINES,
EnvConstants::LOG_NAME, EnvConstants::LOG_NAME,
EnvConstants::LOG_LEVEL EnvConstants::LOG_LEVEL
]); ]);
@ -109,9 +112,43 @@ class BootstrapHelper
$logger->pushProcessor(new UidProcessor()); $logger->pushProcessor(new UidProcessor());
$logger->pushHandler(new ErrorLogHandler(NULL, $logLevelTranslated)); $logger->pushHandler(new ErrorLogHandler(NULL, $logLevelTranslated));
$logPath = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR . 'application.log'; $dir = self::getLogDir();
$logger->pushHandler(new StreamHandler($logPath, $logLevelTranslated)); $path = self::getLogFile();
try {
$fileSystem = new Filesystem();
if (!$fileSystem->exists($dir)) {
$fileSystem->mkdir($dir);
}
if (!$fileSystem->exists($path)) {
$fileSystem->touch($path);
}
} catch (IOException $e) {
die('Could not create logger. Cause: ' . $e->getMessage() . '. Trace: ' . $e->getTraceAsString());
}
$logger->pushHandler(new StreamHandler($path, $logLevelTranslated));
return $logger; return $logger;
} }
/**
* Returns log dir
*
* @return string
*/
public static function getLogDir() {
return __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'log';
}
/**
* Returns log file
*
* @return string
*/
public static function getLogFile() {
return self::getLogDir() . DIRECTORY_SEPARATOR . 'application.log';
}
} }

View file

@ -1,15 +0,0 @@
<div class="modal" id="aboutModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{% trans with {'%name%': getenv('site_title') } %}about.header{% endtrans %}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
{{ 'about.body'|trans|nl2br }}
</div>
</div>
</div>
</div>

View file

@ -8,6 +8,10 @@
{'key': 'duration', 'apply': 'timeInSeconds'}, {'key': 'duration', 'apply': 'timeInSeconds'},
{'key': 'created', 'apply': 'timestamp'} {'key': 'created', 'apply': 'timestamp'}
], ],
'hiddenColumns': ['banid', 'name', 'invokercldbid', 'invokeruid'],
'links': [
{'key': 'invokername', 'uri': '/clients/' ~ sid, 'uri_param': 'invokercldbid'},
],
'additional_links': [ 'additional_links': [
{ {
'header_label': 'bans.delete'|trans, 'header_label': 'bans.delete'|trans,

View file

@ -39,13 +39,9 @@
{% if files|length > 0 %} {% if files|length > 0 %}
<h5>{% trans %}channel_info.h.files{% endtrans %}</h5> <h5>{% trans %}channel_info.h.files{% endtrans %}</h5>
{% include 'table.twig' with {'data': files, {% if files|length > 0 %}
'filters': [ {% include 'files.twig' with {'data': files} %}
{'key': 'datetime', 'apply': 'timestamp'}, {% endif %}
{'key': 'size', 'apply': 'file'},
],
'hiddenColumns': ['cid', 'type']
} %}
{% endif %} {% endif %}
<h5>{% trans %}channel_info.h.details{% endtrans %}</h5> <h5>{% trans %}channel_info.h.details{% endtrans %}</h5>

View file

@ -26,7 +26,8 @@
{% if channels|length > 0 %} {% if channels|length > 0 %}
<h5>{% trans %}channels.h.details{% endtrans %}</h5> <h5>{% trans %}channels.h.details{% endtrans %}</h5>
{% include 'table.twig' with {'data': channels, {% include 'table.twig' with {'data': channels,
'links': [{'key': 'cid', 'uri': '/channels/' ~ sid}], 'links': [{'key': 'channel_name', 'uri': '/channels/' ~ sid, 'uri_param': 'cid'}, {'key': 'pid', 'uri': '/channels/' ~ sid, 'uri_param': 'pid'}],
'hiddenColumns': ['cid'],
'additional_links': [ 'additional_links': [
{ {
'header_label': 'channels.delete'|trans, 'header_label': 'channels.delete'|trans,

View file

@ -9,9 +9,9 @@
{'key': 'client_lastconnected', 'apply': 'timestamp'}, {'key': 'client_lastconnected', 'apply': 'timestamp'},
], ],
'links': [ 'links': [
{'key': 'client_unique_identifier', 'uri': '/clients/' ~ sid, 'uri_param': 'cldbid'}, {'key': 'client_nickname', 'uri': '/clients/' ~ sid, 'uri_param': 'cldbid'},
], ],
'hiddenColumns': ['cldbid'], 'hiddenColumns': ['cldbid', 'client_unique_identifier'],
'additional_links': [ 'additional_links': [
{ {
'header_label': 'clients.delete'|trans, 'header_label': 'clients.delete'|trans,

View file

@ -7,6 +7,11 @@
'filters': [ 'filters': [
{'key': 'timestamp', 'apply': 'timestamp'}, {'key': 'timestamp', 'apply': 'timestamp'},
], ],
'hiddenColumns': ['tcldbid', 'fcldbid'],
'links': [
{'key': 'tname', 'uri': '/clients/' ~ sid, 'uri_param': 'tcldbid'},
{'key': 'fname', 'uri': '/clients/' ~ sid, 'uri_param': 'fcldbid'}
],
'additional_links': [ 'additional_links': [
{ {
'header_label': 'complains.delete'|trans, 'header_label': 'complains.delete'|trans,

View file

@ -0,0 +1,42 @@
<div class="table-responsive">
<table class="table table-sm small sortable-bootstrap table-striped table-bordered" data-sortable>
<thead>
<tr>
<th scope="col">{% trans %}channel_info.files.h.type{% endtrans %}</th>
<th scope="col">{% trans %}channel_info.files.h.path{% endtrans %}</th>
<th scope="col">{% trans %}channel_info.files.h.size{% endtrans %}</th>
<th scope="col">{% trans %}channel_info.files.h.datetime{% endtrans %}</th>
<th scope="col">{% trans %}channel_info.files.h.delete{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% set total = 0 %}
{% for file in data %}
{% set total = total + file.size %}
{% if file.path is empty %}
{% set path = file.name %}
{% elseif file.path == '/' %}
{% set path = '/' ~ file.name %}
{% else %}
{% set path = file.path ~ '/' ~ file.name %}
{% endif %}
<tr>
<td>{{ file.type == 0 ? '<i class="fa fa-folder"></i>' : '<i class="fa fa-file-o"></i>' }}</td>
<td>{{ path }}</td>
<td>{{ file.size == 0 ? '' : file.size|file }}</td>
<td>{{ file.datetime|timestamp }}</td>
<td><a href="/channels/files/delete/{{ sid }}/{{ cid }}?file={{ path }}"><i class="fa fa-trash"></i></a></td>
</tr>
{% endfor %}
<tr>
<td></td>
<td></td>
<td class="font-weight-bold">{{ total|file }}</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>

View file

@ -24,9 +24,14 @@
{% if serverGroups|length > 0 %} {% if serverGroups|length > 0 %}
{% include 'table.twig' with {'data': serverGroups, {% include 'table.twig' with {'data': serverGroups,
'links': [{'key': 'sgid', 'uri': '/servergroups/' ~ sid}], 'hiddenColumns': ['sgid', 'savedb', 'sortid', 'namemode', 'n_modifyp', 'n_member_addp', 'n_member_removep'],
'hiddenColumns': ['savedb', 'sortid', 'namemode', 'n_modifyp', 'n_member_addp', 'n_member_removep'],
'additional_links': [ 'additional_links': [
{
'header_label': 'groups.servergroup'|trans,
'label': '<i class="fa fa-info"></i>',
'uri': '/servergroups/' ~ sid,
'uri_param': 'sgid'
},
{ {
'header_label': 'groups.delete'|trans, 'header_label': 'groups.delete'|trans,
'label': '<i class="fa fa-trash"></i>', 'label': '<i class="fa fa-trash"></i>',
@ -70,9 +75,14 @@
{% if channelGroups|length > 0 %} {% if channelGroups|length > 0 %}
{% include 'table.twig' with {'data': channelGroups, {% include 'table.twig' with {'data': channelGroups,
'links': [{'key': 'cgid', 'uri': '/channelgroups/' ~ sid}], 'hiddenColumns': ['cgid', 'savedb', 'sortid', 'namemode', 'n_modifyp', 'n_member_addp', 'n_member_removep'],
'hiddenColumns': ['savedb', 'sortid', 'namemode', 'n_modifyp', 'n_member_addp', 'n_member_removep'],
'additional_links': [ 'additional_links': [
{
'header_label': 'groups.channelgroup'|trans,
'label': '<i class="fa fa-info"></i>',
'uri': '/channelgroups/' ~ sid,
'uri_param': 'cgid'
},
{ {
'header_label': 'channelgroups.delete'|trans, 'header_label': 'channelgroups.delete'|trans,
'label': '<i class="fa fa-trash"></i>', 'label': '<i class="fa fa-trash"></i>',

View file

@ -58,13 +58,10 @@
</main><!-- /.container --> </main><!-- /.container -->
<!-- Footer --> <!-- Footer -->
{% include 'about.twig' %}
<footer class="footer"> <footer class="footer">
<div class="text-center"> <div class="text-center">
<span class="text-muted"> <span class="text-muted">
<i class="fa fa-copyright"></i> {{ "now"|date("Y") }} {{ getenv('site_title') }} | <i class="fa fa-copyright"></i> {{ "now"|date("Y") }} {{ getenv('site_title') }}
<a data-toggle="modal" data-target="#aboutModal"><i class="fa fa-question"></i></a>
</span> </span>
</div> </div>
</footer> </footer>

View file

@ -4,7 +4,7 @@
<div class="container-fluid h-100"> <div class="container-fluid h-100">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header">{{ title }}</div> <div class="card-header">{{ title }}</div>
<div class="card-body"> <div class="card-body">
@ -12,25 +12,25 @@
<form class="form-signin" role="form" name="login" id="login" method="post"> <form class="form-signin" role="form" name="login" id="login" method="post">
<div class="form-group"> <div class="form-group">
<label for="host" class="col-4 col-form-label">{% trans %}login.form.host{% endtrans %}</label> <label for="host" class="sr-only">{% trans %}login.form.host{% endtrans %}</label>
<input type="text" id="host" name="host" class="form-control form-control-lg" <input type="text" id="host" name="host" class="form-control form-control-sm"
placeholder="{{ getenv('teamspeak_host') ~ ':' ~ getenv('teamspeak_query_port') }}" placeholder="{{ getenv('teamspeak_host') ~ ':' ~ getenv('teamspeak_query_port') }}"
value="{{ getenv('teamspeak_host') ~ ':' ~ getenv('teamspeak_query_port') }}" value="{{ getenv('teamspeak_host') ~ ':' ~ getenv('teamspeak_query_port') }}"
required disabled> required disabled>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="username" class="col-4 col-form-label">{% trans %}login.form.username{% endtrans %}</label> <label for="username" class="sr-only">{% trans %}login.form.username{% endtrans %}</label>
<input type="text" id="username" name="username" class="form-control form-control-lg" <input type="text" id="username" name="username" class="form-control form-control-sm"
placeholder="{{ getenv('teamspeak_user') }}" placeholder="{{ getenv('teamspeak_user') }}"
value="{{ getenv('teamspeak_user') }}" value="{{ getenv('teamspeak_user') }}"
required> required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="col-4 col-form-label">{% trans %}login.form.password{% endtrans %}</label> <label for="password" class="sr-only">{% trans %}login.form.password{% endtrans %}</label>
<input type="password" id="password" name="password" <input type="password" id="password" name="password"
class="form-control form-control-lg" class="form-control form-control-sm"
placeholder="{% trans %}login.form.password.placeholder{% endtrans %}" autofocus required> placeholder="{% trans %}login.form.password.placeholder{% endtrans %}" autofocus required>
</div> </div>

View file

@ -6,8 +6,8 @@
{% if log|length > 0 %} {% if log|length > 0 %}
<div class="small"> <div class="small">
{% for log in data %} {% for line in log %}
<code>{{ log.l }}</code> <code>{{ line.l }}</code><br/>
{% endfor %} {% endfor %}
</div> </div>
@ -15,4 +15,13 @@
{% include 'no_entities.twig' %} {% include 'no_entities.twig' %}
{% endif %} {% endif %}
{% if appLog|length > 0 %}
<br />
<h3>{% trans %}app_log.title{% endtrans %}</h3>
<div class="small">
{% for line in appLog %}
<code>{{ line }}</code><br/>
{% endfor %}
</div>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -27,10 +27,15 @@
{% if data|length > 0 %} {% if data|length > 0 %}
<h5>{% trans %}passwords.h.details{% endtrans %}</h5> <h5>{% trans %}passwords.h.details{% endtrans %}</h5>
{% include 'table.twig' with {'data': data, {% include 'table.twig' with {'data': data,
'hiddenColumns': ['uid', 'tcpw'], 'hiddenColumns': ['tcid', 'uid', 'tcpw'],
'filters': [{'key': 'start', 'apply': 'timestamp'}, {'key': 'end', 'apply': 'timestamp'}], 'filters': [{'key': 'start', 'apply': 'timestamp'}, {'key': 'end', 'apply': 'timestamp'}],
'links': [ 'additional_links': [
{'key': 'tcid', 'uri': '/channels/' ~ sid, 'uri_param': 'tcid'}, {
'header_label': 'passwords.channel'|trans,
'label': '<i class="fa fa-tv"></i>',
'uri': '/channels/' ~ sid,
'uri_param': 'tcid'
}
], ],
'attributesEditable': [ 'attributesEditable': [
{ {

View file

@ -24,7 +24,7 @@
'links': [ 'links': [
{'key': 'client_nickname', 'uri': '/clients/' ~ sid, 'uri_param': 'cldbid'} {'key': 'client_nickname', 'uri': '/clients/' ~ sid, 'uri_param': 'cldbid'}
], ],
'hiddenColumns': ['cldbid'], 'hiddenColumns': ['cldbid', 'client_unique_identifier'],
'additional_links': [ 'additional_links': [
{ {
'header_label': 'servergroup_info.remove'|trans, 'header_label': 'servergroup_info.remove'|trans,