diff --git a/README.md b/README.md index 52bf5ac..fde8772 100755 --- a/README.md +++ b/README.md @@ -94,6 +94,22 @@ location ~ \.php$ { * start server with `php -S localhost:8080 -t public public/index.php` * point browser to [localhost:8080](http://localhost:8080) to have a preview +### 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. + +``` +hiddenDependingOnAttribute // hides a row depending on a value in a table +hiddenColumns // hides an entire column depending on a key in a table +links // generates a link for a specific cell in a table or keyvalue +additional_links // generates extra columns in a table +filters // applies filters depending on a key in a table or key value view +attributesEditable // define editable attributes in the key value view +fields // define fields for a form +``` + +See example usage in the folder `View/material`. + ## Translations ## - This app uses Symfony Translator. It's bootstrapped in `Util\BootstrapHelper` and locales are placed under `data/locale/`. Adjust to your needs or help translating. - Form fields (name/id should be the same) are also translated. For a field named `content` or `ConT enT` translate `form_field_content`. diff --git a/config/ACL.php b/config/ACL.php index 0877017..3c0b0a4 100644 --- a/config/ACL.php +++ b/config/ACL.php @@ -41,6 +41,10 @@ class ACL extends \Zend\Permissions\Acl\Acl '/servers/send/{sid}', '/servers/edit/{sid}', + '/tokens/{sid}', + '/tokens/add/{sid}', + '/tokens/delete/{sid}/{token}', + '/online/{sid}', '/online/{sid}/{clid}', '/online/poke/{sid}/{clid}', diff --git a/config/routes.php b/config/routes.php index 76b7ee4..1154be8 100644 --- a/config/routes.php +++ b/config/routes.php @@ -263,3 +263,19 @@ $container[ChannelSendAction::class] = function ($container) { return new ChannelSendAction($container); }; $app->post('/channels/send/{sid}/{cid}', ChannelSendAction::class); + +// tokens +$container[TokensAction::class] = function ($container) { + return new TokensAction($container); +}; +$app->get('/tokens/{sid}', TokensAction::class); + +$container[TokenAddAction::class] = function ($container) { + return new TokenAddAction($container); +}; +$app->post('/tokens/add/{sid}', TokenAddAction::class); + +$container[TokenDeleteAction::class] = function ($container) { + return new TokenDeleteAction($container); +}; +$app->get('/tokens/delete/{sid}/{token}', TokenDeleteAction::class); diff --git a/data/locale/en.yml b/data/locale/en.yml index f3d3d6d..62c4e20 100644 --- a/data/locale/en.yml +++ b/data/locale/en.yml @@ -63,6 +63,7 @@ menu.servers.clients: "Clients" menu.servers.groups: "Groups" menu.servers.bans: "Bans" menu.servers.complains: "Complains" +menu.servers.tokens: "Tokens" menu.servers.logs: "Log" # titles @@ -81,6 +82,7 @@ complains.title: "Complains" groups.title: "Groups" group_info.title: "Group Info" profile.title: "Profile" +tokens.title: "Tokens" # dynamic render of key value pairs key: "Attribute" @@ -202,4 +204,17 @@ online.send: "Send a message" online.send.message: "Message" online.poked.success: "Poked client %clid%." online.kicked.success: "Kicked client %clid%." -online.banned.success: "Banned client %clid%." \ No newline at end of file +online.banned.success: "Banned client %clid%." + +# tokens +tokens.delete: "Delete" +tokens.h.details: "Details" +tokens.h.add: "Add" +tokens.add: "Add a token" +tokens.add.tokentype: "Type" +tokens.add.serverGroup: "Servergroup (type SERVER has to be selected)" +tokens.add.channelGroup: "Channelgroup (type CHANNEL has to be selected)" +tokens.add.channel: "Channel (type CHANNEL has to be selected)" +tokens.add.description: "Description" +tokens.type.servergroup: "Server: " +tokens.type.channelgroup: "Channel: " \ No newline at end of file diff --git a/public/index.php b/public/index.php index fbe7d86..c72916b 100644 --- a/public/index.php +++ b/public/index.php @@ -110,34 +110,15 @@ $container['view'] = function ($container) use ($app) { )); $view->addExtension(new Twig_Extension_Debug()); - // file size - $fileSizeFilter = new Twig_SimpleFilter('file', function($bytes, $decimals = 2) { - $sz = 'BKMGTP'; - $factor = floor((strlen($bytes) - 1) / 3); - return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; - }); - $view->getEnvironment()->addFilter($fileSizeFilter); - - // time in seconds to human readable - $timeInSecondsFilter = new Twig_SimpleFilter('timeInSeconds', function($seconds) use ($container) { - return $container['ts']->getInstance()->convertSecondsToStrTime($seconds); - }); - $view->getEnvironment()->addFilter($timeInSecondsFilter); - - $timeInMillisFilter = new Twig_SimpleFilter('timeInMillis', function($millis) use ($container) { - return $container['ts']->getInstance()->convertSecondsToStrTime(floor($millis/1000)); - }); - $view->getEnvironment()->addFilter($timeInMillisFilter); - - // timestamp to carbon - $timestampFilter = new Twig_SimpleFilter('timestamp', function($timestamp) { - return Carbon::createFromTimestamp($timestamp); - }); - $view->getEnvironment()->addFilter($timestampFilter); - // dynamically apply filters $view->getEnvironment()->addExtension(new ApplyFilterExtension()); + // encode url + $encodeUrl = new Twig_SimpleFilter('escape_url', function($str) { + return urlencode($str); + }); + $view->getEnvironment()->addFilter($encodeUrl); + // translation $view->addExtension(new \Symfony\Bridge\Twig\Extension\TranslationExtension($container['translator'])); $view->getEnvironment()->getExtension('Twig_Extension_Core')->setDateFormat(getenv('site_date_format')); @@ -158,6 +139,55 @@ $container['view'] = function ($container) use ($app) { return $container['session']->get($key); })); + // ts specific: file size + $fileSizeFilter = new Twig_SimpleFilter('file', function($bytes, $decimals = 2) { + $sz = 'BKMGTP'; + $factor = floor((strlen($bytes) - 1) / 3); + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; + }); + $view->getEnvironment()->addFilter($fileSizeFilter); + + // ts specific: time in seconds to human readable + $timeInSecondsFilter = new Twig_SimpleFilter('timeInSeconds', function($seconds) use ($container) { + return $container['ts']->getInstance()->convertSecondsToStrTime($seconds); + }); + $view->getEnvironment()->addFilter($timeInSecondsFilter); + + $timeInMillisFilter = new Twig_SimpleFilter('timeInMillis', function($millis) use ($container) { + return $container['ts']->getInstance()->convertSecondsToStrTime(floor($millis/1000)); + }); + $view->getEnvironment()->addFilter($timeInMillisFilter); + + // ts specific: timestamp to carbon + $timestampFilter = new Twig_SimpleFilter('timestamp', function($timestamp) { + return Carbon::createFromTimestamp($timestamp); + }); + $view->getEnvironment()->addFilter($timestampFilter); + + // ts specific: token type + $tokenTypeFilter = new Twig_SimpleFilter('tokentype', function($type) { + $tokenTypes = TSInstance::getTokenTypes(); + + foreach ($tokenTypes as $name => $tokenType) { + if ($type == $tokenType) return $name; + } + + return $type; + }); + $view->getEnvironment()->addFilter($tokenTypeFilter); + + // ts specific: group type + $groupTypeFilter = new Twig_SimpleFilter('permgrouptype', function($type) { + $groupTypes = TSInstance::getPermGroupTypes(); + + foreach ($groupTypes as $name => $groupType) { + if ($type == $groupType) return $name; + } + + return $type; + }); + $view->getEnvironment()->addFilter($groupTypeFilter); + // flash $view['flash'] = $container['flash']; diff --git a/src/Control/Actions/TokenAddAction.php b/src/Control/Actions/TokenAddAction.php new file mode 100644 index 0000000..7095bf0 --- /dev/null +++ b/src/Control/Actions/TokenAddAction.php @@ -0,0 +1,36 @@ +getParsedBody(); + $type = $body['tokentype']; + $serverGroup = $body['serverGroup']; + $channelGroup = $body['channelGroup']; + $channel = $body['channel']; + $description = $body['description']; + + $this->logger->debug('Body', $body); + + $this->ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + + $tokenAddResult = $this->ts->getInstance()->tokenAdd( + $type, + ($type == ts3admin::TokenServerGroup ? $serverGroup : $channelGroup), + $channel, + $description + ); + + $this->flash->addMessage('success', $this->translator->trans('added')); + + return $response->withRedirect('/tokens/' . $sid); + } +} \ No newline at end of file diff --git a/src/Control/Actions/TokenDeleteAction.php b/src/Control/Actions/TokenDeleteAction.php new file mode 100644 index 0000000..017651c --- /dev/null +++ b/src/Control/Actions/TokenDeleteAction.php @@ -0,0 +1,22 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $tokenDeleteResult = $this->ts->getInstance()->tokenDelete($token); + + $this->flash->addMessage('success', $this->translator->trans('done')); + + return $response->withRedirect('/tokens/' . $sid); + } +} \ No newline at end of file diff --git a/src/Control/Actions/TokensAction.php b/src/Control/Actions/TokensAction.php new file mode 100644 index 0000000..ac6dda9 --- /dev/null +++ b/src/Control/Actions/TokensAction.php @@ -0,0 +1,56 @@ +ts->login($this->auth->getIdentity()['user'], $this->auth->getIdentity()['password']); + $selectResult = $this->ts->getInstance()->selectServer($sid, 'serverId'); + + $dataResult = $this->ts->getInstance()->tokenList(); + + // channels + $channelsResult = $this->ts->getInstance()->channelList(); + $channelsResult = $this->ts->getInstance()->getElement('data', $channelsResult); + $channels = []; + foreach ($channelsResult as $channel) { + $channels[$channel['channel_name']] = $channel['cid']; + } + + // groups + $serverGroups = []; + + $serverGroupsResult = $this->ts->getInstance()->serverGroupList(); + $serverGroupsResult = $this->ts->getInstance()->getElement('data', $serverGroupsResult); + + foreach ($serverGroupsResult as $group) { + $serverGroups[$group['name']] = $group['sgid']; + } + arsort($serverGroups); + + $channelGroups = []; + $channelGroupsResult = $this->ts->getInstance()->channelGroupList(); + $channelGroupsResult = $this->ts->getInstance()->getElement('data', $channelGroupsResult); + + foreach ($channelGroupsResult as $group) { + $channelGroups[$group['name']] = $group['cgid']; + } + arsort($channelGroups); + + // render GET + $this->view->render($response, 'tokens.twig', [ + 'title' => $this->translator->trans('tokens.title'), + 'data' => $this->ts->getInstance()->getElement('data', $dataResult), + 'tokentypes' => TSInstance::getTokenTypes(), + 'channels' => $channels, + 'serverGroups' => $serverGroups, + 'channelGroups' => $channelGroups, + 'sid' => $sid + ]); + } +} \ No newline at end of file diff --git a/src/Util/TSInstance.php b/src/Util/TSInstance.php index 014cc20..73ee346 100644 --- a/src/Util/TSInstance.php +++ b/src/Util/TSInstance.php @@ -178,4 +178,31 @@ class TSInstance return $arr; } + + /** + * @return array + */ + public static function getTokenTypes() + { + $arr = []; + $arr['TokenServerGroup'] = ts3admin::TokenServerGroup; + $arr['TokenChannelGroup'] = ts3admin::TokenChannelGroup; + + return $arr; + } + + /** + * @return array + */ + public static function getPermGroupTypes() + { + $arr = []; + $arr['PermGroupTypeServerGroup'] = ts3admin::PermGroupTypeServerGroup; + $arr['PermGroupTypeGlobalClient'] = ts3admin::PermGroupTypeGlobalClient; + $arr['PermGroupTypeChannel'] = ts3admin::PermGroupTypeChannel; + $arr['PermGroupTypeChannelGroup'] = ts3admin::PermGroupTypeChannelGroup; + $arr['PermGroupTypeChannelClient'] = ts3admin::PermGroupTypeChannelClient; + + return $arr; + } } \ No newline at end of file diff --git a/src/View/material/channel_info.twig b/src/View/material/channel_info.twig index d36b293..6f08ad6 100644 --- a/src/View/material/channel_info.twig +++ b/src/View/material/channel_info.twig @@ -5,7 +5,7 @@

{% trans %}channel_info.h.actions{% endtrans %}

{% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'channel_info.send'|trans, 'label': 'check_circle', @@ -20,7 +20,7 @@ {% if clients|length > 0 %}

{% trans %}channel_info.h.clients{% endtrans %}

{% include 'table.twig' with {'data': clients, - 'hide': [{'key': 'client_type', 'values': ['1']}], + 'hiddenDependingOnAttribute': [{'key': 'client_type', 'values': ['1']}], 'links': [ {'key': 'clid', 'uri': '/online/' ~ sid}, {'key': 'client_database_id', 'uri': '/clients/' ~ sid, 'uri_param': 'client_database_id'} diff --git a/src/View/material/client_info.twig b/src/View/material/client_info.twig index 47f4a88..8714ebf 100644 --- a/src/View/material/client_info.twig +++ b/src/View/material/client_info.twig @@ -4,7 +4,7 @@

{{ title }}

{% trans %}client_info.h.actions{% endtrans %}

{% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'client_info.ban'|trans, 'label': 'check_circle', diff --git a/src/View/material/form.twig b/src/View/material/form.twig index e3fc85a..85a7be7 100644 --- a/src/View/material/form.twig +++ b/src/View/material/form.twig @@ -1,4 +1,4 @@ -{% for form in additional_forms %} +{% for form in fields %}

{{ form.header_label }}

{% set item = "#{form.uri}" %} @@ -16,12 +16,23 @@ {% for field in form.fields %}
+
- + {% if field.type == 'select' %} + + + + {% else %} + + {% endif %}
{% endfor %} diff --git a/src/View/material/group_info.twig b/src/View/material/group_info.twig index bce6712..69b35ee 100644 --- a/src/View/material/group_info.twig +++ b/src/View/material/group_info.twig @@ -5,7 +5,7 @@

{% trans %}group_info.h.clients_add{% endtrans %}

{% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'group_info.add'|trans, 'label': 'check_circle', diff --git a/src/View/material/layout/menu.twig b/src/View/material/layout/menu.twig index a2b0cbd..5ffe12c 100644 --- a/src/View/material/layout/menu.twig +++ b/src/View/material/layout/menu.twig @@ -37,6 +37,9 @@
  • {% trans %}menu.servers.complains{% endtrans %}
  • + +
  • {% trans %}menu.servers.tokens{% endtrans %}
  • +
  • {% trans %}menu.servers.logs{% endtrans %}
  • diff --git a/src/View/material/online.twig b/src/View/material/online.twig index cb45270..e082664 100644 --- a/src/View/material/online.twig +++ b/src/View/material/online.twig @@ -10,7 +10,8 @@
    {% if data|length >0 %} {% include 'table.twig' with {'data': data, - 'hide': [{'key': 'client_type', 'values': ['1']}], + 'hiddenDependingOnAttribute': [{'key': 'client_type', 'values': ['1']}], + 'hiddenColumns': ['client_type'], 'filters': [ {'key': 'client_idle_time', 'apply': 'timeInMillis'}, {'key': 'connection_connected_time', 'apply': 'timeInSeconds'}, diff --git a/src/View/material/online_info.twig b/src/View/material/online_info.twig index 23f53c1..cc07b96 100644 --- a/src/View/material/online_info.twig +++ b/src/View/material/online_info.twig @@ -5,7 +5,7 @@

    {% trans %}online_info.h.actions{% endtrans %}

    {% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'online.poke'|trans, 'label': 'check_circle', diff --git a/src/View/material/server_info.twig b/src/View/material/server_info.twig index 8ba1e6d..5f1fe75 100644 --- a/src/View/material/server_info.twig +++ b/src/View/material/server_info.twig @@ -10,7 +10,7 @@

    {% trans %}server_info.h.actions{% endtrans %}

    {% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'server_info.send'|trans, 'label': 'check_circle', diff --git a/src/View/material/servers.twig b/src/View/material/servers.twig index 88737ab..023ee50 100644 --- a/src/View/material/servers.twig +++ b/src/View/material/servers.twig @@ -46,7 +46,7 @@

    {% trans %}servers.h.create{% endtrans %}

    {% include 'form.twig' with { - 'additional_forms': [ + 'fields': [ { 'header_label': 'server_create.label'|trans, 'label': 'check_circle', diff --git a/src/View/material/table.twig b/src/View/material/table.twig index 16f509b..3304e48 100644 --- a/src/View/material/table.twig +++ b/src/View/material/table.twig @@ -8,7 +8,9 @@ {% endfor %} {% for key in added %} - {{ key|replace({'_' : ' '})|title }} + {% if key not in hiddenColumns %} + {{ key|replace({'_' : ' '})|title }} + {% endif %} {% endfor %} {% for link in additional_links %} @@ -19,62 +21,77 @@ {% for arr in data %} - {% set show = true %} - {% for hidden in hide %} - {% if hidden.key in arr|keys and attribute(arr, hidden.key) in hidden.values %} - {% set show = false %} - {% endif %} - {% endfor %} + {% set show = true %} - {% if show == true %} - - {% for key, value in arr %} - {% set value = value %} + {% for hidden in hiddenDependingOnAttribute %} + {% if hidden.key in arr|keys and attribute(arr, hidden.key) in hidden.values %} + {% set show = false %} + {% endif %} + {% endfor %} - {% for filter in filters %} - {% if filter.key == key %} - {% set value = value|apply_filter(filter.apply)|raw %} + {% if show == true %} + + {% for key, value in arr %} + {% set value = value %} + + {% for filter in filters %} + {% if filter.key == key %} + {% set value = value|apply_filter(filter.apply)|raw %} + {% endif %} + {% endfor %} + + {% set showColumn = true %} + + {% if key in hiddenColumns %} + {% set showColumn = false %} + {% endif %} + + {% if showColumn %} + {% set item = '' ~ value ~ '' %} + + {% for link in links %} + {% if link.key == key %} + {% if link.uri_param is not empty %} + {% for searchingKey, searchingValue in arr %} + {% if searchingKey == link.uri_param %} + {% set item = "#{value}" %} + {% endif %} + {% endfor %} + {% else %} + {% set item = "#{value}" %} + {% endif %} + {% endif %} + {% endfor %} + + {{ item|raw }} {% endif %} {% endfor %} - {% set item = '' ~ value ~ '' %} + {% for link in additional_links %} + + + {% set item = "#{link.label}" %} - {% for link in links %} - {% if link.key == key %} {% if link.uri_param is not empty %} {% for searchingKey, searchingValue in arr %} {% if searchingKey == link.uri_param %} - {% set item = "#{value}" %} + {% set shownValue = searchingValue %} + + {% if link.apply is not empty %} + {% set shownValue = shownValue|apply_filter(link.apply)|raw %} + {% endif %} + + {% set item = "#{link.label}" %} {% endif %} {% endfor %} - {% else %} - {% set item = "#{value}" %} {% endif %} - {% endif %} + + {{ item|raw }} + {% endfor %} - - {{ item|raw }} - {% endfor %} - - {% for link in additional_links %} - - - {% set item = "#{link.label}" %} - - {% if link.uri_param is not empty %} - {% for searchingKey, searchingValue in arr %} - {% if searchingKey == link.uri_param %} - {% set item = "#{link.label}" %} - {% endif %} - {% endfor %} - {% endif %} - - {{ item|raw }} - - {% endfor %} - - {% endif %} + + {% endif %} {% endfor %} diff --git a/src/View/material/tokens.twig b/src/View/material/tokens.twig new file mode 100644 index 0000000..afc1802 --- /dev/null +++ b/src/View/material/tokens.twig @@ -0,0 +1,44 @@ +{% extends 'layout.twig' %} + +{% block content %} +

    {{ title }}

    + +

    {% trans %}tokens.h.add{% endtrans %}

    + {% include 'form.twig' with { + 'fields': [ + { + 'header_label': 'tokens.add'|trans, + 'label': 'check_circle', + 'uri': '/tokens/add/' ~ sid, + 'uri_method': 'post', + 'fields': [ + {'type': 'select', 'key': 'tokentype', 'options': tokentypes, 'label': 'tokens.add.tokentype'|trans}, + {'type': 'select', 'key': 'serverGroup', 'options': serverGroups, 'label': 'tokens.add.serverGroup'|trans}, + {'type': 'select', 'key': 'channelGroup', 'options': channelGroups, 'label': 'tokens.add.channelGroup'|trans}, + {'type': 'select', 'key': 'channel', 'options': channels, 'label': 'tokens.add.channel'|trans}, + {'type': 'text', 'key': 'description', 'label': 'tokens.add.description'|trans}, + ] + } + ] + } %} + + {% if data|length >0 %} +

    {% trans %}tokens.h.details{% endtrans %}

    + {% include 'table.twig' with {'data': data, + 'filters': [ + {'key': 'token_created', 'apply': 'timestamp'}, + {'key': 'token_type', 'apply': 'tokentype'}, + ], + 'hiddenColumns': ['token_id1','token_id2'], + 'additional_links': [ + { + 'header_label': 'tokens.delete'|trans, + 'label': 'delete', + 'uri': '/tokens/delete/' ~ sid, + 'uri_param': 'token', + 'apply': 'escape_url' + } + ], + } %} + {% endif %} +{% endblock %} \ No newline at end of file