mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-19 02:54:38 +00:00
Proxy server WIP
This commit is contained in:
parent
dbbceca0bf
commit
869f61c23a
@ -30,7 +30,7 @@ namespace OPNsense\Proxy\Api;
|
||||
|
||||
use \OPNsense\Base\ApiControllerBase;
|
||||
use \OPNsense\Core\Backend;
|
||||
use \OPNsense\Proxy\General;
|
||||
use \OPNsense\Proxy\Proxy;
|
||||
|
||||
/**
|
||||
* Class ServiceController
|
||||
@ -101,13 +101,13 @@ class ServiceController extends ApiControllerBase
|
||||
// close session for long running action
|
||||
session_write_close();
|
||||
|
||||
$mdlGeneral = new General();
|
||||
$mdlProxy = new Proxy();
|
||||
$backend = new Backend();
|
||||
|
||||
$runStatus = $this->statusAction();
|
||||
|
||||
// stop squid when disabled
|
||||
if ($runStatus['status'] == "running" && $mdlGeneral->enabled->__toString() == 0) {
|
||||
if ($runStatus['status'] == "running" && $mdlProxy->general->enabled->__toString() == 0) {
|
||||
$this->stopAction();
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ class ServiceController extends ApiControllerBase
|
||||
$backend->sendEvent("template reload OPNsense.Proxy");
|
||||
|
||||
// (res)start daemon
|
||||
if ($mdlGeneral->enabled->__toString() == 1) {
|
||||
if ($mdlProxy->general->enabled->__toString() == 1) {
|
||||
if ($runStatus['status'] == "running") {
|
||||
$backend->sendEvent("service reconfigure proxy");
|
||||
} else {
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
namespace OPNsense\Proxy\Api;
|
||||
|
||||
use \OPNsense\Base\ApiControllerBase;
|
||||
use \OPNsense\Proxy\General;
|
||||
use \OPNsense\Proxy\Proxy;
|
||||
use \OPNsense\Core\Config;
|
||||
|
||||
/**
|
||||
@ -39,19 +39,40 @@ use \OPNsense\Core\Config;
|
||||
class SettingsController extends ApiControllerBase
|
||||
{
|
||||
/**
|
||||
* retrieve general settings
|
||||
* retrieve proxy settings
|
||||
* @return array
|
||||
*/
|
||||
public function getAction()
|
||||
{
|
||||
$result = array();
|
||||
if ($this->request->isGet()) {
|
||||
$mdlGeneral = new General();
|
||||
$mdlProxy = new Proxy();
|
||||
|
||||
$selopt=array("lan"=>"LAN","wan"=>"WAN");
|
||||
$mdlGeneral->interfaces->setSelectOptions($selopt);
|
||||
// Define array for selected interfaces
|
||||
$selopt=Array();
|
||||
|
||||
$result['general'] = $mdlGeneral->getNodes();
|
||||
// Get ConfigObject
|
||||
$configObj = Config::getInstance()->object();
|
||||
// Iterate over all interfaces configuration
|
||||
// TODO: replace for <interfaces> helper
|
||||
foreach ( $configObj->interfaces->children() as $key => $value ) {
|
||||
// Check if interface is enabled, if tag is <enable/> treat as enabled.
|
||||
if ( isset($value->enable) && ( $value->enable != '0' ) ) {
|
||||
// Check if interface has static ip
|
||||
if ($value->ipaddr != 'dhcp') {
|
||||
|
||||
if ($value->descr == '') {
|
||||
$description = strtoupper($key); // Use interface name as description if none is given
|
||||
} else {
|
||||
$description = $value->descr;
|
||||
}
|
||||
$selopt[$key] = (string)$description; // Add Interface to selectable options.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$mdlProxy->forward->interfaces->setSelectOptions($selopt);
|
||||
$result['proxy'] = $mdlProxy->getNodes();
|
||||
}
|
||||
|
||||
return $result;
|
||||
@ -66,23 +87,23 @@ class SettingsController extends ApiControllerBase
|
||||
public function setAction()
|
||||
{
|
||||
$result = array("result"=>"failed");
|
||||
if ($this->request->hasPost("general")) {
|
||||
if ($this->request->hasPost("proxy")) {
|
||||
// load model and update with provided data
|
||||
$mdlGeneral = new General();
|
||||
$mdlGeneral->setNodes($this->request->getPost("general"));
|
||||
$mdlProxy = new Proxy();
|
||||
$mdlProxy->setNodes($this->request->getPost("proxy"));
|
||||
|
||||
// perform validation
|
||||
$valMsgs = $mdlGeneral->performValidation();
|
||||
$valMsgs = $mdlProxy->performValidation();
|
||||
foreach ($valMsgs as $field => $msg) {
|
||||
if (!array_key_exists("validations", $result)) {
|
||||
$result["validations"] = array();
|
||||
}
|
||||
$result["validations"]["general.".$msg->getField()] = $msg->getMessage();
|
||||
$result["validations"]["proxy.".$msg->getField()] = $msg->getMessage();
|
||||
}
|
||||
|
||||
// serialize model to config
|
||||
if ($valMsgs->count() == 0) {
|
||||
$mdlGeneral->serializeToConfig();
|
||||
$mdlProxy->serializeToConfig();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ class IndexController extends \OPNsense\Base\IndexController
|
||||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->title = "Proxy";
|
||||
$this->view->title = "Proxy Server";
|
||||
$this->view->pick('OPNsense/Proxy/index');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
<model>
|
||||
<mount>//OPNsense/proxy/general</mount>
|
||||
<description>
|
||||
(squid) proxy general settings
|
||||
</description>
|
||||
<items>
|
||||
<enabled type="BooleanField">
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</enabled>
|
||||
<interfaces type="CSVListField">
|
||||
<Required>N</Required>
|
||||
</interfaces>
|
||||
<port type="IntegerField">
|
||||
<default>3128</default>
|
||||
<MinimumValue>1</MinimumValue>
|
||||
<MaximumValue>65535</MaximumValue>
|
||||
<Required>Y</Required>
|
||||
</port>
|
||||
</items>
|
||||
</model>
|
||||
@ -30,6 +30,6 @@ namespace OPNsense\Proxy;
|
||||
|
||||
use OPNsense\Base\BaseModel;
|
||||
|
||||
class General extends BaseModel
|
||||
class Proxy extends BaseModel
|
||||
{
|
||||
}
|
||||
37
src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml
Normal file
37
src/opnsense/mvc/app/models/OPNsense/Proxy/Proxy.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<model>
|
||||
<mount>//OPNsense/proxy</mount>
|
||||
<description>
|
||||
(squid) proxy settings
|
||||
</description>
|
||||
<items>
|
||||
<general>
|
||||
<enabled type="BooleanField">
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</enabled>
|
||||
</general>
|
||||
<forward>
|
||||
<interfaces type="CSVListField">
|
||||
<Required>N</Required>
|
||||
</interfaces>
|
||||
<port type="IntegerField">
|
||||
<default>3128</default>
|
||||
<MinimumValue>1</MinimumValue>
|
||||
<MaximumValue>65535</MaximumValue>
|
||||
<ValidationMessage>"Proxy port needs to be an integer value between 1 and 65535"</ValidationMessage>
|
||||
<Required>Y</Required>
|
||||
</port>
|
||||
<addACLforInterfaceSubnets type="BooleanField">
|
||||
<default>1</default>
|
||||
<Required>Y</Required>
|
||||
</addACLforInterfaceSubnets>
|
||||
<transparentProxyMode type="BooleanField">
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</transparentProxyMode>
|
||||
<alternateDNSservers type="CSVListField">
|
||||
<Required>N</Required>
|
||||
</alternateDNSservers>
|
||||
</forward>
|
||||
</items>
|
||||
</model>
|
||||
@ -1,29 +1,64 @@
|
||||
<script type="text/javascript">
|
||||
|
||||
$( document ).ready(function() {
|
||||
|
||||
data_get_map = {'frm_proxy':"/api/proxy/settings/get"};
|
||||
|
||||
// load initial data
|
||||
ajaxGet(url="/api/proxy/settings/get",sendData={},callback=function(data,status) {
|
||||
if (status == "success") {
|
||||
setFormData('frm_general',data);
|
||||
}
|
||||
$.each(data_get_map, function(data_index, data_url) {
|
||||
ajaxGet(url=data_url,sendData={},callback=function(data,status) {
|
||||
if (status == "success") {
|
||||
$("form").each(function( index ) {
|
||||
if ( $(this).attr('id').split('-')[0] == data_index) {
|
||||
// related form found, load data
|
||||
setFormData($(this).attr('id'),data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// form event handlers
|
||||
$("#save").click(function(){
|
||||
saveFormToEndpoint(url="/api/proxy/settings/set",formid="frm_general",callback_ok=function(){
|
||||
$("#save_proxy-general").click(function(){
|
||||
|
||||
// save data for General TAB
|
||||
saveFormToEndpoint(url="/api/proxy/settings/set",formid="frm_proxy-general",callback_ok=function(){
|
||||
// on correct save, perform reconfigure. set progress animation when reloading
|
||||
$("#frm_general_progress").addClass("fa fa-spinner fa-pulse");
|
||||
$("#frm_proxy-general_progress").addClass("fa fa-spinner fa-pulse");
|
||||
|
||||
//
|
||||
ajaxCall(url="/api/proxy/service/reconfigure", sendData={}, callback=function(data,status){
|
||||
// when done, disable progress animation.
|
||||
$("#frm_general_progress").removeClass("fa fa-spinner fa-pulse");
|
||||
$("#frm_proxy-general_progress").removeClass("fa fa-spinner fa-pulse");
|
||||
|
||||
if (status != "success" || data['status'] != 'ok' ) {
|
||||
// fix error handling
|
||||
BootstrapDialog.show({
|
||||
type:BootstrapDialog.TYPE_WARNING,
|
||||
title: 'Proxy',
|
||||
title: 'Proxy General TAB',
|
||||
message: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
$("#save_proxy-forward").click(function(){
|
||||
// save data for Proxy TAB
|
||||
saveFormToEndpoint(url="/api/proxy/settings/set",formid="frm_proxy-forward",callback_ok=function(){
|
||||
// on correct save, perform reconfigure. set progress animation when reloading
|
||||
$("#frm_proxy-forward_progress").addClass("fa fa-spinner fa-pulse");
|
||||
|
||||
//
|
||||
ajaxCall(url="/api/proxy/service/reconfigure", sendData={}, callback=function(data,status){
|
||||
// when done, disable progress animation.
|
||||
$("#frm_proxy-forward_progress").removeClass("fa fa-spinner fa-pulse");
|
||||
|
||||
if (status != "success" || data['status'] != 'ok' ) {
|
||||
// fix error handling
|
||||
BootstrapDialog.show({
|
||||
type:BootstrapDialog.TYPE_WARNING,
|
||||
title: 'Proxy Server TAB',
|
||||
message: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
@ -32,53 +67,109 @@
|
||||
});
|
||||
});
|
||||
|
||||
// handle help messages show/hide
|
||||
$('[id*="show_all_help"]').click(function() {
|
||||
$('[id*="show_all_help"]').toggleClass("fa-toggle-on fa-toggle-off");
|
||||
$('[id*="show_all_help"]').toggleClass("text-success text-danger");
|
||||
if ($('[id*="show_all_help"]').hasClass("fa-toggle-on")) {
|
||||
$('[for*="help_for"]').addClass("show");
|
||||
$('[for*="help_for"]').removeClass("hidden");
|
||||
} else {
|
||||
$('[for*="help_for"]').addClass("hidden");
|
||||
$('[for*="help_for"]').removeClass("show");
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function(){
|
||||
$('select[class="tokenize"]').each(function(){
|
||||
if ($(this).prop("size")==0) {
|
||||
//number_of_items = $(this).children('option').length;
|
||||
maxDropdownHeight=String(36*5)+"px"; // default number of items
|
||||
|
||||
} else {
|
||||
number_of_items = $(this).prop("size");
|
||||
maxDropdownHeight=String(36*number_of_items)+"px";
|
||||
}
|
||||
hint=$(this).data("hint");
|
||||
width=$(this).data("width");
|
||||
allownew=$(this).data("allownew");
|
||||
maxTokenContainerHeight=$(this).data("maxheight");
|
||||
|
||||
$(this).tokenize({
|
||||
displayDropdownOnFocus: true,
|
||||
newElements: allownew,
|
||||
placeholder:hint
|
||||
});
|
||||
$(this).parent().find('ul[class="TokensContainer"]').parent().css("width",width);
|
||||
$(this).parent().find('ul[class="Dropdown"]').css("max-height", maxDropdownHeight);
|
||||
if ( maxDropdownHeight != undefined ) {
|
||||
$(this).parent().find('ul[class="TokensContainer"]').css("max-height", maxTokenContainerHeight);
|
||||
}
|
||||
})
|
||||
},500);
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<ul class="nav nav-tabs nav-justified" role="tablist" id="maintabs">
|
||||
<li class="active"><a data-toggle="tab" href="#tabGeneral">General</a></li>
|
||||
<li><a data-toggle="tab" href="#sectionB">Section B</a></li>
|
||||
</ul>
|
||||
<div class="content-box tab-content">
|
||||
<div id="tabGeneral" class="tab-pane fade in active">
|
||||
<form id="frm_general" class="form-inline">
|
||||
<table class="table table-striped table-condensed table-responsive">
|
||||
<colgroup>
|
||||
<col class="col-md-3"/>
|
||||
<col class="col-md-4"/>
|
||||
<col class="col-md-5"/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{{ partial("layout_partials/form_input_tr",
|
||||
['id': 'general.enabled',
|
||||
'label':'enabled',
|
||||
'type':'checkbox',
|
||||
'help':'test'
|
||||
])
|
||||
}}
|
||||
{{ partial("layout_partials/form_input_tr",
|
||||
['id': 'general.interfaces',
|
||||
'label':'interfaces',
|
||||
'type':'select_multiple'
|
||||
])
|
||||
}}
|
||||
{{ partial("layout_partials/form_input_tr",
|
||||
['id': 'general.port',
|
||||
'label':'port',
|
||||
'type':'text'
|
||||
])
|
||||
}}
|
||||
<!-- TODO: explain TABS
|
||||
content_location,tab_name,
|
||||
field_array
|
||||
activetab: content_location
|
||||
-->
|
||||
<!-- TODO: explain usage of select_multiple
|
||||
special options:
|
||||
style: used as class, defined classes are: tokenize
|
||||
hint: show default text used for tokenize select
|
||||
allownew: set to "true" if new items can be added to the list, default is "false"
|
||||
size: for tokenize this defines the max shown items (default = 5) of the dropdown list, if it does not fit a scrollbar is shown
|
||||
maxheight: define max height of select box, default=170px to hold 5 items
|
||||
-->
|
||||
|
||||
<tr>
|
||||
<td colspan="3"><button class="btn btn-primary" id="save" type="button">Apply <i id="frm_general_progress" class=""></i></button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div id="sectionB" class="tab-pane fade">
|
||||
{{ partial("layout_partials/base_tabs",
|
||||
['tabs': {
|
||||
['proxy-general','General Proxy Settings',
|
||||
{['id': 'proxy.general.enabled',
|
||||
'label':'Enable proxy',
|
||||
'type':'checkbox',
|
||||
'help':'Enable or disable the proxy service.'
|
||||
]}
|
||||
],
|
||||
['proxy-forward','Forward Proxy',
|
||||
{['id': 'proxy.forward.interfaces',
|
||||
'label':'Proxy interfaces',
|
||||
'type':'select_multiple',
|
||||
'style':'tokenize',
|
||||
'help':'Select interface(s) the proxy will bind to.',
|
||||
'hint':'Type or select interface'
|
||||
],
|
||||
['id': 'proxy.forward.port',
|
||||
'label':'Proxy port',
|
||||
'type':'text',
|
||||
'help':'The port the proxy service will listen to.'
|
||||
],
|
||||
['id': 'proxy.forward.addACLforInterfaceSubnets',
|
||||
'label':'Allow interface subnets',
|
||||
'type':'checkbox',
|
||||
'help':'When enabled the subnets of the selected interfaces will be added to the allow access list.'
|
||||
],
|
||||
['id': 'proxy.forward.transparentProxyMode',
|
||||
'label':'Enable Transparent HTTP proxy',
|
||||
'type':'checkbox',
|
||||
'help':'Enable transparent proxe mode to forward all requests for destination port 80 to the proxy server without any additional configuration.'
|
||||
],
|
||||
['id': 'proxy.forward.alternateDNSservers',
|
||||
'label':'Use alternate DNS-servers',
|
||||
'type':'select_multiple',
|
||||
'style':'tokenize',
|
||||
'help':'Type IPs of alternative DNS servers you like to use.',
|
||||
'hint':'Type or select interface',
|
||||
'allownew':'true'
|
||||
]}
|
||||
]
|
||||
},
|
||||
'activetab':'proxy-general'
|
||||
])
|
||||
}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
35
src/opnsense/mvc/app/views/layout_partials/base_tabs.volt
Normal file
35
src/opnsense/mvc/app/views/layout_partials/base_tabs.volt
Normal file
@ -0,0 +1,35 @@
|
||||
<ul class="nav nav-tabs " role="tablist" id="maintabs">
|
||||
{% for tab in tabs|default([]) %}
|
||||
<li {% if activetab|default("") == tab[0] %} class="active" {% endif %}><a data-toggle="tab" href="#tab_{{tab[0]}}"><b>{{tab[1]}}</b></a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="content-box tab-content">
|
||||
{% for tab in tabs|default([]) %}
|
||||
<div id="tab_{{tab[0]}}" class="tab-pane fade{% if activetab|default("") == tab[0] %} in active {% endif %}">
|
||||
<form id="frm_{{tab[0]}}" class="form-inline">
|
||||
<table class="table table-striped table-condensed table-responsive">
|
||||
<colgroup>
|
||||
<col class="col-md-3"/>
|
||||
<col class="col-md-4"/>
|
||||
<col class="col-md-5"/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="3" align="right">
|
||||
<small>{{ lang._('toggle full help on/off') }} </small><a href="#"><i class="fa fa-toggle-off text-danger" id="show_all_help_{{tab[0]}}" type="button"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for field in tab[2]|default({})%}
|
||||
{{ partial("layout_partials/form_input_tr",field)}}
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="3"><button class="btn btn-primary" id="save_{{tab[0]}}" type="button">Apply <i id="frm_{{tab[0]}}_progress" class=""></i></button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -1,8 +1,12 @@
|
||||
<tr for="{{ id }}">
|
||||
<td >
|
||||
<div class="control-label" for="{{ id }}">
|
||||
{{label}}
|
||||
{% if help|default(false) %} <a id="help_for_{{ id }}" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> {% endif %}
|
||||
{% if help|default(false) %}
|
||||
<a id="help_for_{{ id }}" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a>
|
||||
{% elseif help|default(false) == false %}
|
||||
<i class="fa fa-info-circle text-muted"></i>
|
||||
{% endif %}
|
||||
<b>{{label}}</b>
|
||||
</div>
|
||||
</td>
|
||||
<td >
|
||||
@ -11,11 +15,10 @@
|
||||
{% elseif type == "checkbox" %}
|
||||
<input type="checkbox" id="{{ id }}" >
|
||||
{% elseif type == "select_multiple" %}
|
||||
<select multiple="multiple" size="{{size|default(2)}}" id="{{ id }}"></select>
|
||||
<select multiple="multiple" {% if size|default(false) %}size="{{size}}"{% endif %} id="{{ id }}" {% if style|default(false) %}class="{{style}}" {% endif %} {% if hint|default(false) %}data-hint="{{hint}}"{% endif %} {% if maxheight|default(false) %}data-maxheight="{{maxheight}}"{% endif %} data-width="{{width|default("348px")}}" data-allownew="{{allownew|default("false")}}"></select>
|
||||
{% endif %}
|
||||
|
||||
{% if help|default(false) %}
|
||||
<br/>
|
||||
<small class="hidden" for="help_for_{{ id }}" >{{help}}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@ -1 +1 @@
|
||||
<input type="{{field_type}}" value="{{field_content}}" name="{{field_name_prefix}}{{field_name}}" >
|
||||
<input type="{{field_type}}" value="{{field_content}}" name="{{field_name_prefix}}{{field_name}}" class="text-info">
|
||||
|
||||
@ -4,19 +4,21 @@
|
||||
|
||||
# setup listen configuration
|
||||
{% if helpers.exists('OPNsense.proxy.general.port') %}
|
||||
{% for interface in OPNsense.proxy.general.interfaces.split(",") %}
|
||||
{% for intf_key,intf_item in interfaces.iteritems() %}
|
||||
{% if intf_key == interface and intf_item.ipaddr != 'dhcp' %}
|
||||
{% for interface in OPNsense.proxy.general.interfaces.split(",") %}
|
||||
{% for intf_key,intf_item in interfaces.iteritems() %}
|
||||
{% if intf_key == interface and intf_item.ipaddr != 'dhcp' %}
|
||||
http_port {{intf_item.ipaddr}}:{{ OPNsense.proxy.general.port }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# virtual ip's #}
|
||||
{% for intf_key,intf_item in virtualip.iteritems() %}
|
||||
{% if intf_item.interface == interface and intf_item.mode == 'ipalias' %}
|
||||
{% if helpers.exists('virtualip') %}
|
||||
{% for intf_key,intf_item in virtualip.iteritems() %}
|
||||
{% if intf_item.interface == interface and intf_item.mode == 'ipalias' %}
|
||||
http_port {{intf_item.subnet}}:{{ OPNsense.proxy.general.port }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@ -29,7 +29,9 @@ div.Tokenize ul.TokensContainer
|
||||
{
|
||||
cursor: text;
|
||||
padding: 0 5px 0 0;
|
||||
height: 100px;
|
||||
height:auto;
|
||||
min-height:34px;
|
||||
max-height: 170px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
}
|
||||
@ -138,6 +140,9 @@ div.Tokenize ul.Dropdown
|
||||
border-radius: 0 0 6px 6px;
|
||||
|
||||
z-index: 20;
|
||||
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
div.Tokenize ul.Dropdown li
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user