mirror of
https://github.com/lucaspalomodevelop/opnsense-core.git
synced 2026-03-13 00:07:27 +00:00
Compare commits
21 Commits
9fe8d18942
...
c48353cdc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48353cdc1 | ||
|
|
8d6ca1fa98 | ||
|
|
ad09e7aa6c | ||
|
|
b2dc6fed7c | ||
|
|
d8ecd8c31b | ||
|
|
3a9e9edefe | ||
|
|
433d8d62b3 | ||
|
|
51a5118d6e | ||
|
|
2774a9b498 | ||
|
|
e4203d81eb | ||
|
|
de5dd5f527 | ||
|
|
7fc2ab43a4 | ||
|
|
a7cb604301 | ||
|
|
92881adb40 | ||
|
|
d73ec9feae | ||
|
|
8db4e28614 | ||
|
|
b163c68bf9 | ||
|
|
7dae89eadf | ||
|
|
fd98874ce7 | ||
|
|
e57aeea3e8 | ||
|
|
7f9444f754 |
2
plist
2
plist
@ -1095,6 +1095,7 @@
|
||||
/usr/local/opnsense/scripts/firmware/bogons.sh
|
||||
/usr/local/opnsense/scripts/firmware/changelog.sh
|
||||
/usr/local/opnsense/scripts/firmware/check.sh
|
||||
/usr/local/opnsense/scripts/firmware/cleanup.sh
|
||||
/usr/local/opnsense/scripts/firmware/config.sh
|
||||
/usr/local/opnsense/scripts/firmware/connection.sh
|
||||
/usr/local/opnsense/scripts/firmware/details.sh
|
||||
@ -1472,6 +1473,7 @@
|
||||
/usr/local/opnsense/site-python/daemonize.py
|
||||
/usr/local/opnsense/site-python/duckdb_helper.py
|
||||
/usr/local/opnsense/site-python/log_helper.py
|
||||
/usr/local/opnsense/site-python/params.py
|
||||
/usr/local/opnsense/site-python/sqlite3_helper.py
|
||||
/usr/local/opnsense/site-python/tls_helper.py
|
||||
/usr/local/opnsense/site-python/watchers/__init__.py
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014-2023 Deciso B.V.
|
||||
* Copyright (C) 2014-2025 Deciso B.V.
|
||||
* Copyright (C) 2010 Ermal Luçi
|
||||
* Copyright (C) 2007-2008 Scott Ullrich <sullrich@gmail.com>
|
||||
* Copyright (C) 2005-2006 Bill Marquette <bill.marquette@gmail.com>
|
||||
@ -50,6 +50,7 @@ $userindex = index_users();
|
||||
function isAuthLocalIP($http_host)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if (isset($config['virtualip']['vip'])) {
|
||||
foreach ($config['virtualip']['vip'] as $vip) {
|
||||
if ($vip['subnet'] == $http_host) {
|
||||
@ -57,6 +58,7 @@ function isAuthLocalIP($http_host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$address_in_list = function ($interface_list_ips, $http_host) {
|
||||
foreach ($interface_list_ips as $ilips => $ifname) {
|
||||
// remove scope from link-local IPv6 addresses
|
||||
@ -66,11 +68,13 @@ function isAuthLocalIP($http_host)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// try using cached addresses
|
||||
$interface_list_ips = get_cached_json_content("/tmp/isAuthLocalIP.cache.json");
|
||||
if (!empty($interface_list_ips) && $address_in_list($interface_list_ips, $http_host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fetch addresses and store in cache
|
||||
$interface_list_ips = get_configured_ip_addresses();
|
||||
file_put_contents("/tmp/isAuthLocalIP.cache.json", json_encode($interface_list_ips));
|
||||
@ -82,9 +86,11 @@ function index_groups()
|
||||
{
|
||||
global $config, $groupindex;
|
||||
|
||||
$groupindex = array();
|
||||
$groupindex = [];
|
||||
|
||||
if (isset($config['system']['group'])) {
|
||||
$i = 0;
|
||||
|
||||
foreach ($config['system']['group'] as $groupent) {
|
||||
if (isset($groupent['name'])) {
|
||||
$groupindex[$groupent['name']] = $i;
|
||||
@ -93,7 +99,7 @@ function index_groups()
|
||||
}
|
||||
}
|
||||
|
||||
return ($groupindex);
|
||||
return $groupindex;
|
||||
}
|
||||
|
||||
function index_users()
|
||||
@ -104,10 +110,12 @@ function index_users()
|
||||
|
||||
if (!empty($config['system']['user'])) {
|
||||
$i = 0;
|
||||
|
||||
foreach ($config['system']['user'] as $userent) {
|
||||
if (!empty($userent) && !empty($userent['name'])) {
|
||||
$userindex[$userent['name']] = $i;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
@ -118,7 +126,9 @@ function index_users()
|
||||
function getUserGroups($username)
|
||||
{
|
||||
global $config;
|
||||
$member_groups = array();
|
||||
|
||||
$member_groups = [];
|
||||
|
||||
$user = getUserEntry($username);
|
||||
if ($user !== false) {
|
||||
$allowed_groups = local_user_get_groups($user);
|
||||
@ -130,18 +140,20 @@ function getUserGroups($username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $member_groups;
|
||||
}
|
||||
|
||||
function &getUserEntry($name)
|
||||
{
|
||||
global $config, $userindex;
|
||||
$false = false;
|
||||
|
||||
if (isset($userindex[$name])) {
|
||||
return $config['system']['user'][$userindex[$name]];
|
||||
} else {
|
||||
return $false;
|
||||
}
|
||||
|
||||
$ret = false; /* XXX "fixes" return by reference */
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function &getUserEntryByUID($uid)
|
||||
@ -156,7 +168,8 @@ function &getUserEntryByUID($uid)
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
$ret = false; /* XXX "fixes" return by reference */
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function &getGroupEntry($name)
|
||||
@ -167,7 +180,8 @@ function &getGroupEntry($name)
|
||||
return $config['system']['group'][$groupindex[$name]];
|
||||
}
|
||||
|
||||
return array();
|
||||
$ret = []; /* XXX "fixes" return by reference */
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function get_user_privileges(&$user)
|
||||
@ -189,6 +203,7 @@ function get_user_privileges(&$user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $privs;
|
||||
}
|
||||
|
||||
@ -212,9 +227,7 @@ function userHasPrivilege($userent, $privid = false)
|
||||
|
||||
function userIsAdmin($username)
|
||||
{
|
||||
$user = getUserEntry($username);
|
||||
|
||||
return userHasPrivilege($user, 'page-all');
|
||||
return userHasPrivilege(getUserEntry($username), 'page-all');
|
||||
}
|
||||
|
||||
function local_sync_accounts()
|
||||
@ -289,8 +302,6 @@ function local_sync_accounts()
|
||||
|
||||
function local_user_set(&$user, $force_password = false, $userattrs = null)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if (empty($user['password'])) {
|
||||
auth_log("Cannot set user {$user['name']}: password is missing");
|
||||
return;
|
||||
@ -305,7 +316,7 @@ function local_user_set(&$user, $force_password = false, $userattrs = null)
|
||||
$user_pass = $force_password ? $user['password'] : '*';
|
||||
$user_name = $user['name'];
|
||||
$user_uid = $user['uid'];
|
||||
$comment = str_replace(array(':', '!', '@'), ' ', $user['descr']);
|
||||
$comment = str_replace([':', '!', '@'], ' ', $user['descr']);
|
||||
|
||||
$lock_account = 'lock';
|
||||
|
||||
@ -398,7 +409,7 @@ function local_user_set(&$user, $force_password = false, $userattrs = null)
|
||||
@unlink("{$user_home}/.ssh/authorized_keys");
|
||||
}
|
||||
|
||||
mwexecf('/usr/sbin/pw %s %s', array($lock_account, $user_name), true);
|
||||
mwexecf('/usr/sbin/pw %s %s', [$lock_account, $user_name], true);
|
||||
}
|
||||
|
||||
function local_user_set_password(&$user, $password = null)
|
||||
@ -494,7 +505,16 @@ function local_group_set($group)
|
||||
$group_op = 'groupmod';
|
||||
}
|
||||
|
||||
mwexecf('/usr/sbin/pw %s %s -g %s -M %s', array($group_op, $group_name, $group_gid, $group_members));
|
||||
mwexecf('/usr/sbin/pw %s %s -g %s -M %s', [$group_op, $group_name, $group_gid, $group_members]);
|
||||
}
|
||||
|
||||
function auth_get_authserver_local()
|
||||
{
|
||||
return [
|
||||
'host' => $config['system']['hostname'],
|
||||
'name' => gettext('Local Database'),
|
||||
'type' => 'local',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -505,12 +525,8 @@ function auth_get_authserver($name)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($name == "Local Database") {
|
||||
return array(
|
||||
"name" => gettext("Local Database"),
|
||||
"type" => "local",
|
||||
"host" => $config['system']['hostname']
|
||||
);
|
||||
if ($name == 'Local Database') {
|
||||
return auth_get_authserver_local();
|
||||
}
|
||||
|
||||
if (!empty($config['system']['authserver'])) {
|
||||
@ -537,7 +553,7 @@ function auth_get_authserver_list()
|
||||
{
|
||||
global $config;
|
||||
|
||||
$list = array();
|
||||
$list = [];
|
||||
|
||||
if (!empty($config['system']['authserver'])) {
|
||||
foreach ($config['system']['authserver'] as $authcfg) {
|
||||
@ -546,7 +562,8 @@ function auth_get_authserver_list()
|
||||
}
|
||||
}
|
||||
|
||||
$list["Local Database"] = array( "name" => gettext("Local Database"), "type" => "local", "host" => $config['system']['hostname']);
|
||||
$list['Local Database'] = auth_get_authserver_local();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
||||
@ -180,11 +180,13 @@ function _dnsmasq_add_host_entries()
|
||||
}
|
||||
|
||||
foreach ($dnsmasqcfg['hosts'] as $host) {
|
||||
/* The host.ip field supports multiple IPv4 and IPv6 addresses */
|
||||
foreach (explode(',', $host['ip']) as $ip) {
|
||||
if (!empty($host['host']) && empty($host['domain'])) {
|
||||
$lhosts .= "{$host['ip']}\t{$host['host']}\n";
|
||||
$lhosts .= "{$ip}\t{$host['host']}\n";
|
||||
} elseif (!empty($host['host'])) {
|
||||
/* XXX: The question is if we do want "host" as a global alias */
|
||||
$lhosts .= "{$host['ip']}\t{$host['host']}.{$host['domain']} {$host['host']}\n";
|
||||
$lhosts .= "{$ip}\t{$host['host']}.{$host['domain']} {$host['host']}\n";
|
||||
}
|
||||
if (!empty($host['aliases'])) {
|
||||
foreach (explode(",", $host['aliases']) as $alias) {
|
||||
@ -192,7 +194,8 @@ function _dnsmasq_add_host_entries()
|
||||
* XXX: pre migration all hosts where added here as alias, when we combine host.domain we
|
||||
* miss some information, which is likely not a very good idea anyway.
|
||||
*/
|
||||
$lhosts .= "{$host['ip']}\t{$alias}\n";
|
||||
$lhosts .= "{$ip}\t{$alias}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) 2015-2023 Franco Fichtner <franco@opnsense.org>
|
||||
* Copyright (c) 2015-2025 Franco Fichtner <franco@opnsense.org>
|
||||
* Copyright (c) 2015-2018 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
@ -492,6 +492,36 @@ class FirmwareController extends ApiMutableModelControllerBase
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* run an audit in the backend
|
||||
* @return array status
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function auditHelper(string $audit): array
|
||||
{
|
||||
$backend = new Backend();
|
||||
$response = [];
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$response['status'] = 'ok';
|
||||
$response['msg_uuid'] = trim($backend->configdRun("firmware $audit", true));
|
||||
} else {
|
||||
$response['status'] = 'failure';
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* run a cleanup task
|
||||
* @return array status
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function cleanupAction()
|
||||
{
|
||||
return $this->auditHelper('cleanup');
|
||||
}
|
||||
|
||||
/**
|
||||
* run a connection check
|
||||
* @return array status
|
||||
@ -499,17 +529,7 @@ class FirmwareController extends ApiMutableModelControllerBase
|
||||
*/
|
||||
public function connectionAction()
|
||||
{
|
||||
$backend = new Backend();
|
||||
$response = array();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$response['status'] = 'ok';
|
||||
$response['msg_uuid'] = trim($backend->configdRun("firmware connection", true));
|
||||
} else {
|
||||
$response['status'] = 'failure';
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $this->auditHelper('connection');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -519,17 +539,7 @@ class FirmwareController extends ApiMutableModelControllerBase
|
||||
*/
|
||||
public function healthAction()
|
||||
{
|
||||
$backend = new Backend();
|
||||
$response = array();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$response['status'] = 'ok';
|
||||
$response['msg_uuid'] = trim($backend->configdRun("firmware health", true));
|
||||
} else {
|
||||
$response['status'] = 'failure';
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $this->auditHelper('health');
|
||||
}
|
||||
|
||||
/*
|
||||
@ -539,18 +549,11 @@ class FirmwareController extends ApiMutableModelControllerBase
|
||||
*/
|
||||
public function auditAction()
|
||||
{
|
||||
$backend = new Backend();
|
||||
$response = array();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$this->getLogger('audit')->notice(sprintf("[Firmware] User %s executed a security audit", $this->getUserName()));
|
||||
$response['status'] = 'ok';
|
||||
$response['msg_uuid'] = trim($backend->configdRun("firmware audit", true));
|
||||
} else {
|
||||
$response['status'] = 'failure';
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $this->auditHelper('audit');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2016 Deciso B.V.
|
||||
* Copyright (C) 2016-2025 Deciso B.V.
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@ -70,6 +70,7 @@ class NetworkinsightController extends ApiControllerBase
|
||||
$to_date = $filter->sanitize($to_date, "int");
|
||||
$resolution = $filter->sanitize($resolution, "int");
|
||||
$field = $filter->sanitize($field, "string");
|
||||
$interfaces = $this->getInterfacesAction();
|
||||
|
||||
$result = array();
|
||||
if ($this->request->isGet()) {
|
||||
@ -78,11 +79,6 @@ class NetworkinsightController extends ApiControllerBase
|
||||
$response = $backend->configdRun(
|
||||
"netflow aggregate fetch {$provider} {$from_date} {$to_date} {$resolution} {$field}"
|
||||
);
|
||||
// for test, request random data
|
||||
//$response = $backend->configdRun(
|
||||
// "netflow aggregate fetch {$provider} {$from_date} {$to_date} {$resolution} {$field} " .
|
||||
// "em0,in~em0,out~em1,in~em1,out~em2,in~em2,out~em3,in~em3,out"
|
||||
//);
|
||||
$graph_data = json_decode($response, true);
|
||||
if ($graph_data != null) {
|
||||
ksort($graph_data);
|
||||
@ -124,7 +120,19 @@ class NetworkinsightController extends ApiControllerBase
|
||||
}
|
||||
}
|
||||
foreach ($timeseries as $timeserie_key => $data) {
|
||||
$result[] = array("key" => $timeserie_key, "values" => $data);
|
||||
$record = [
|
||||
"key" => $timeserie_key,
|
||||
"values" => $data
|
||||
];
|
||||
if (in_array($provider, ['FlowInterfaceTotals'])) {
|
||||
$tmp = explode(',', $timeserie_key);
|
||||
if (!empty($interfaces[$tmp[0]])) {
|
||||
$record['interface'] = $interfaces[$tmp[0]];
|
||||
}
|
||||
$record['direction'] = $tmp[1] ?? '';
|
||||
}
|
||||
|
||||
$result[] = $record;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,6 +167,8 @@ class NetworkinsightController extends ApiControllerBase
|
||||
$max_hits = $filter->sanitize($max_hits, "int");
|
||||
|
||||
if ($this->request->isGet()) {
|
||||
$protocols = $this->getProtocolsAction();
|
||||
$services = $this->getServicesAction();
|
||||
if ($this->request->get("filter_field") != null && $this->request->get("filter_value") != null) {
|
||||
$filter_fields = explode(',', $this->request->get("filter_field"));
|
||||
$filter_values = explode(',', $this->request->get("filter_value"));
|
||||
@ -181,7 +191,26 @@ class NetworkinsightController extends ApiControllerBase
|
||||
$configd_cmd .= " {$measure} {$data_filter} {$max_hits}";
|
||||
$response = $backend->configdRun($configd_cmd);
|
||||
$graph_data = json_decode($response, true);
|
||||
if ($graph_data != null) {
|
||||
if (is_array($graph_data)) {
|
||||
foreach ($graph_data as &$record) {
|
||||
if (isset($record['dst_port']) || isset($record['service_port'])) {
|
||||
$portnum = $record['dst_port'] ?? $record['service_port'];
|
||||
$label = $portnum;
|
||||
$protocol = '';
|
||||
if (isset($record['protocol']) && isset($protocols[$record['protocol']])) {
|
||||
$protocol = sprintf(" (%s)", $protocols[$record['protocol']]);
|
||||
}
|
||||
if (isset($services[$portnum])) {
|
||||
$label = $services[$portnum];
|
||||
}
|
||||
$record['last_seen_str'] = '';
|
||||
if (!empty($record['last_seen'])) {
|
||||
$record['last_seen_str'] = date('Y-m-d H:i:s', $record['last_seen']);
|
||||
}
|
||||
|
||||
$record['label'] = $label . $protocol;
|
||||
}
|
||||
}
|
||||
return $graph_data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,39 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
protected static $internalModelClass = '\OPNsense\Dnsmasq\Dnsmasq';
|
||||
protected static $internalModelUseSafeDelete = true;
|
||||
|
||||
/**
|
||||
* Tags and interface filter function.
|
||||
* Interfaces are tags too, in the sense of dnsmasq.
|
||||
*
|
||||
* @return callable|null
|
||||
*/
|
||||
private function buildFilterFunction(): ?callable
|
||||
{
|
||||
$filterValues = $this->request->get('tags') ?? [];
|
||||
$fieldNames = ['interface', 'set_tag', 'tag'];
|
||||
if (empty($filterValues)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return function ($record) use ($filterValues, $fieldNames) {
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
// Skip this field if not present in current record
|
||||
if (!isset($record->{$fieldName})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match field values against filter list
|
||||
foreach (array_map('trim', explode(',', (string)$record->{$fieldName})) as $value) {
|
||||
if (in_array($value, $filterValues, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@ -51,7 +84,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* hosts */
|
||||
public function searchHostAction()
|
||||
{
|
||||
return $this->searchBase('hosts');
|
||||
return $this->searchBase('hosts', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getHostAction($uuid = null)
|
||||
@ -137,7 +170,18 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp tags */
|
||||
public function searchTagAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_tags');
|
||||
$filters = $this->request->get('tags') ?? [];
|
||||
|
||||
$filter_funct = null;
|
||||
if (!empty($filters)) {
|
||||
$filter_funct = function ($record) use ($filters) {
|
||||
$attributes = $record->getAttributes();
|
||||
$uuid = $attributes['uuid'] ?? null;
|
||||
return in_array($uuid, $filters, true);
|
||||
};
|
||||
}
|
||||
|
||||
return $this->searchBase('dhcp_tags', null, null, $filter_funct);
|
||||
}
|
||||
|
||||
public function getTagAction($uuid = null)
|
||||
@ -163,7 +207,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp ranges */
|
||||
public function searchRangeAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_ranges');
|
||||
return $this->searchBase('dhcp_ranges', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getRangeAction($uuid = null)
|
||||
@ -189,7 +233,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp options */
|
||||
public function searchOptionAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_options');
|
||||
return $this->searchBase('dhcp_options', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getOptionAction($uuid = null)
|
||||
@ -215,7 +259,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp match options */
|
||||
public function searchMatchAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_options_match');
|
||||
return $this->searchBase('dhcp_options_match', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getMatchAction($uuid = null)
|
||||
@ -241,7 +285,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp boot options */
|
||||
public function searchBootAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_boot');
|
||||
return $this->searchBase('dhcp_boot', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getBootAction($uuid = null)
|
||||
@ -263,4 +307,50 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
{
|
||||
return $this->delBase('dhcp_boot', $uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selectpicker options for interfaces and tags
|
||||
*/
|
||||
public function getTagListAction()
|
||||
{
|
||||
$result = [
|
||||
'tags' => [
|
||||
'label' => gettext('Tags'),
|
||||
'icon' => 'fa fa-tag text-primary',
|
||||
'items' => []
|
||||
],
|
||||
'interfaces' => [
|
||||
'label' => gettext('Interfaces'),
|
||||
'icon' => 'fa fa-ethernet text-info',
|
||||
'items' => []
|
||||
]
|
||||
];
|
||||
|
||||
// Interfaces
|
||||
foreach (Config::getInstance()->object()->interfaces->children() as $key => $intf) {
|
||||
if ((string)$intf->type === 'group') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['interfaces']['items'][] = [
|
||||
'value' => $key,
|
||||
'label' => empty($intf->descr) ? strtoupper($key) : (string)$intf->descr
|
||||
];
|
||||
}
|
||||
|
||||
// Tags
|
||||
foreach ($this->getModel()->dhcp_tags->iterateItems() as $uuid => $tag) {
|
||||
$result['tags']['items'][] = [
|
||||
'value' => $uuid,
|
||||
'label' => (string)$tag->tag
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_keys($result) as $key) {
|
||||
usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label']));
|
||||
}
|
||||
|
||||
// Assemble result
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,15 +17,18 @@
|
||||
</field>
|
||||
<field>
|
||||
<id>host.ip</id>
|
||||
<label>IP address</label>
|
||||
<type>text</type>
|
||||
<help>IP address of the host, e.g. 192.168.100.100 or fd00:abcd::1</help>
|
||||
<label>IP addresses</label>
|
||||
<type>select_multiple</type>
|
||||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help>IP addresses of the host, e.g. 192.168.100.100 or fd00:abcd::1. Can be multiple IPv4 and IPv6 addresses for dual stack configurations. Setting multiple addresses will automatically assign the best match based on the subnet of the interface receiving the DHCP Discover.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.aliases</id>
|
||||
<label>Aliases</label>
|
||||
<type>select_multiple</type>
|
||||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help>list of aliases (fqdn)</help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
@ -36,16 +39,48 @@
|
||||
<label>DHCP</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.hwaddr</id>
|
||||
<label>Hardware address</label>
|
||||
<id>host.client_id</id>
|
||||
<label>Client identifier</label>
|
||||
<type>text</type>
|
||||
<help>When offered and the client requests an address via dhcp, assign the address provided here</help>
|
||||
<help>Match the identifier of the client, e.g., DUID for DHCPv6. Setting the special character "*" will ignore the client identifier for DHCPv4 leases if a client offers both as choice.</help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.hwaddr</id>
|
||||
<label>Hardware addresses</label>
|
||||
<type>select_multiple</type>
|
||||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help>Match the hardware address of the client. Can be multiple addresses, e.g., if the client has multiple network cards. Though keep in mind that DNSmasq cannot assume which address is the correct one when multiple send DHCP Discover at the same time.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.lease_time</id>
|
||||
<label>Lease time</label>
|
||||
<type>text</type>
|
||||
<hint>86400</hint>
|
||||
<help>Defines how long the addresses (leases) given out by the server are valid (in seconds)</help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.set_tag</id>
|
||||
<label>Tag [set]</label>
|
||||
<type>dropdown</type>
|
||||
<help>Optional tag to set for requests matching this range which can be used to selectively match dhcp options</help>
|
||||
<help>Optional tag to set for requests matching this range which can be used to selectively match dhcp options. Can be left empty if options with an interface tag exist, since the client automatically receives this tag based on the interface receiving the DHCP Discover.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>host.ignore</id>
|
||||
<label>Ignore</label>
|
||||
<type>checkbox</type>
|
||||
<help>Ignore any DHCP packets of this host. Useful if it should get served by a different DHCP server.</help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<type>boolean</type>
|
||||
<formatter>boolean</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
|
||||
@ -342,7 +342,8 @@ class FilterController extends FilterBaseController
|
||||
'items' => [
|
||||
[
|
||||
'value' => '',
|
||||
'label' => gettext('Any')
|
||||
'label' => gettext('Any'),
|
||||
'selected' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
@ -55,24 +55,46 @@ class Dnsmasq extends BaseModel
|
||||
|
||||
$messages = parent::performValidation($validateFullModel);
|
||||
|
||||
$usedDhcpIpAddresses = [];
|
||||
foreach ($this->hosts->iterateItems() as $host) {
|
||||
if (!$host->hwaddr->isEmpty() || !$host->client_id->isEmpty()) {
|
||||
foreach (explode(',', (string)$host->ip) as $ip) {
|
||||
$usedDhcpIpAddresses[$ip] = isset($usedDhcpIpAddresses[$ip]) ? $usedDhcpIpAddresses[$ip] + 1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->hosts->iterateItems() as $host) {
|
||||
if (!$validateFullModel && !$host->isFieldChanged()) {
|
||||
continue;
|
||||
}
|
||||
$key = $host->__reference;
|
||||
if (!$host->hwaddr->isEmpty() && strpos($host->ip->getCurrentValue(), ':') !== false) {
|
||||
|
||||
// all dhcp-host IP addresses must be unique, host overrides can have duplicate IP addresses
|
||||
if (!$host->hwaddr->isEmpty() || !$host->client_id->isEmpty()) {
|
||||
foreach (explode(',', (string)$host->ip) as $ip) {
|
||||
if ($usedDhcpIpAddresses[$ip] > 1) {
|
||||
$messages->appendMessage(
|
||||
new Message(
|
||||
gettext("Only IPv4 reservations are currently supported"),
|
||||
sprintf(gettext("'%s' is already used in another DHCP host entry."), $ip),
|
||||
$key . ".ip"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($host->host->isEmpty() && $host->hwaddr->isEmpty()) {
|
||||
if (
|
||||
$host->host->isEmpty() &&
|
||||
$host->hwaddr->isEmpty() &&
|
||||
$host->client_id->isEmpty()
|
||||
) {
|
||||
$messages->appendMessage(
|
||||
new Message(
|
||||
gettext("Hostnames my only be omitted when a hardware address is offered."),
|
||||
gettext(
|
||||
"Hostnames may only be omitted when either a hardware address " .
|
||||
"or a client identifier is provided."
|
||||
),
|
||||
$key . ".host"
|
||||
)
|
||||
);
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
</local_ttl>
|
||||
<add_mac type="OptionField">
|
||||
<OptionValues>
|
||||
<default>default</default>
|
||||
<standard>standard</standard>
|
||||
<base64>base64</base64>
|
||||
<text>text</text>
|
||||
</OptionValues>
|
||||
@ -82,8 +82,21 @@
|
||||
<ip type="NetworkField">
|
||||
<Required>Y</Required>
|
||||
<NetMaskAllowed>N</NetMaskAllowed>
|
||||
<FieldSeparator>,</FieldSeparator>
|
||||
<AsList>Y</AsList>
|
||||
</ip>
|
||||
<hwaddr type="MacAddressField"/>
|
||||
<client_id type="TextField">
|
||||
<Mask>/^(?:\*|(?:[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2})+))$/</Mask>
|
||||
<ValidationMessage>Value must be a colon-separated hexadecimal sequence (e.g., 01:02:f3) or "*".</ValidationMessage>
|
||||
</client_id>
|
||||
<hwaddr type="MacAddressField">
|
||||
<FieldSeparator>,</FieldSeparator>
|
||||
<AsList>Y</AsList>
|
||||
</hwaddr>
|
||||
<lease_time type="IntegerField">
|
||||
<MinimumValue>1</MinimumValue>
|
||||
</lease_time>
|
||||
<ignore type="BooleanField"/>
|
||||
<set_tag type="ModelRelationField">
|
||||
<Model>
|
||||
<tag>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<name>VPN: IPsec: Edit Pre-Shared Keys</name>
|
||||
<patterns>
|
||||
<pattern>ui/ipsec/pre_shared_keys</pattern>
|
||||
<pattern>api/ipsec/pre_shared_keys</pattern>
|
||||
<pattern>api/ipsec/pre_shared_keys/*</pattern>
|
||||
</patterns>
|
||||
</page-vpn-ipsec-editkeys>
|
||||
<page-vpn-ipsec-mobile>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<Mobile order="20" VisibleName="Mobile Clients (legacy)" url="/vpn_ipsec_mobile.php">
|
||||
<Act url="/vpn_ipsec_mobile.php*" visibility="hidden"/>
|
||||
</Mobile>
|
||||
<Keys order="30" VisibleName="Pre-Shared Keys" url="/ui/ipsec/pre_shared_keys/"/>
|
||||
<Keys order="30" VisibleName="Pre-Shared Keys" url="/ui/ipsec/pre_shared_keys"/>
|
||||
<KeyPairs order="40" VisibleName="Key Pairs" url="/ui/ipsec/key_pairs" />
|
||||
<Settings order="50" VisibleName="Mobile & Advanced Settings" url="/ui/ipsec/connections/settings"/>
|
||||
<Status order="60" VisibleName="Status Overview" url="/ui/ipsec/sessions"/>
|
||||
|
||||
@ -344,7 +344,6 @@
|
||||
</Constraints>
|
||||
</mx>
|
||||
<ttl type="IntegerField">
|
||||
<Required>N</Required>
|
||||
<!-- https://datatracker.ietf.org/doc/html/rfc2181 -->
|
||||
<MaximumValue>2147483647</MaximumValue>
|
||||
<MinimumValue>0</MinimumValue>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{#
|
||||
# Copyright (c) 2015-2023 Franco Fichtner <franco@opnsense.org>
|
||||
# Copyright (c) 2015-2025 Franco Fichtner <franco@opnsense.org>
|
||||
# Copyright (c) 2015-2018 Deciso B.V.
|
||||
# All rights reserved.
|
||||
#
|
||||
@ -619,9 +619,10 @@
|
||||
$("#plugin_see").click(function () { $('#plugintab > a').tab('show'); });
|
||||
$("#plugin_get").click(function () { backend('syncPlugins'); });
|
||||
$("#plugin_set").click(function () { backend('resyncPlugins'); });
|
||||
$('#audit_security').click(function () { backend('audit'); });
|
||||
$('#audit_cleanup').click(function () { backend('cleanup'); });
|
||||
$('#audit_connection').click(function () { backend('connection'); });
|
||||
$('#audit_health').click(function () { backend('health'); });
|
||||
$('#audit_security').click(function () { backend('audit'); });
|
||||
$('#audit_upgrade').click(function () {
|
||||
ajaxCall('/api/core/firmware/log/0', {}, function (data, status) {
|
||||
if (data['log'] != undefined) {
|
||||
@ -904,6 +905,7 @@
|
||||
<i class="fa fa-lock"></i> {{ lang._('Run an audit') }} <i class="caret"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a id="audit_cleanup" href="#">{{ lang._('Cleanup') }}</a></li>
|
||||
<li><a id="audit_connection" href="#">{{ lang._('Connectivity') }}</a></li>
|
||||
<li><a id="audit_health" href="#">{{ lang._('Health') }}</a></li>
|
||||
<li><a id="audit_security" href="#">{{ lang._('Security') }}</a></li>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{#
|
||||
|
||||
OPNsense® is Copyright © 2016 by Deciso B.V.
|
||||
OPNsense® is Copyright © 2016-2025 by Deciso B.V.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -55,15 +55,8 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
// collect all chars for resize update
|
||||
var pageCharts = {};
|
||||
|
||||
// form metadata definitions
|
||||
var interface_names = [];
|
||||
var service_names = [];
|
||||
var protocol_names = [];
|
||||
|
||||
/**
|
||||
* load shared metadata (interfaces, protocols, )
|
||||
*/
|
||||
function get_metadata()
|
||||
function do_startup()
|
||||
{
|
||||
var dfObj = new $.Deferred();
|
||||
ajaxGet('/api/diagnostics/netflow/isEnabled', {}, function(is_enabled, status){
|
||||
@ -73,13 +66,12 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
}
|
||||
// fetch interface names
|
||||
ajaxGet('/api/diagnostics/networkinsight/getInterfaces',{}, function(intf_names, status){
|
||||
interface_names = intf_names;
|
||||
// fetch protocol names
|
||||
ajaxGet('/api/diagnostics/networkinsight/getProtocols',{}, function(protocols, status) {
|
||||
protocol_names = protocols;
|
||||
// fetch service names
|
||||
ajaxGet('/api/diagnostics/networkinsight/getServices',{}, function(services, status) {
|
||||
service_names = services;
|
||||
for (var key in intf_names) {
|
||||
$('#interface_select').append($("<option></option>").attr("value",key).text(intf_names[key]));
|
||||
$('#interface_select_detail').append($("<option></option>").attr("value",key).text(intf_names[key]));
|
||||
}
|
||||
$('#interface_select').selectpicker('refresh');
|
||||
$('#interface_select_detail').selectpicker('refresh');
|
||||
// return promise, no need to wait for getMetadata
|
||||
dfObj.resolve();
|
||||
// fetch aggregators
|
||||
@ -101,8 +93,6 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return dfObj;
|
||||
}
|
||||
@ -123,51 +113,9 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
function get_time_select()
|
||||
{
|
||||
// current time stamp
|
||||
var timestamp_now = Math.round((new Date()).getTime() / 1000);
|
||||
var duration = 0;
|
||||
var resolution = 0;
|
||||
switch ($("#total_time_select").val()) {
|
||||
case "2h":
|
||||
duration = 60*60*2;
|
||||
resolution = 30;
|
||||
break;
|
||||
case "8h":
|
||||
duration = 60*60*8;
|
||||
resolution = 300;
|
||||
break;
|
||||
case "24h":
|
||||
duration = 60*60*24;
|
||||
resolution = 300;
|
||||
break;
|
||||
case "7d":
|
||||
duration = 60*60*24*7;
|
||||
resolution = 3600;
|
||||
break;
|
||||
case "14d":
|
||||
duration = 60*60*24*14;
|
||||
resolution = 3600;
|
||||
break;
|
||||
case "30d":
|
||||
duration = 60*60*24*30;
|
||||
resolution = 86400;
|
||||
break;
|
||||
case "60d":
|
||||
duration = 60*60*24*60;
|
||||
resolution = 86400;
|
||||
break;
|
||||
case "90d":
|
||||
duration = 60*60*24*90;
|
||||
resolution = 86400;
|
||||
break;
|
||||
case "182d":
|
||||
duration = 60*60*24*182;
|
||||
resolution = 86400;
|
||||
break;
|
||||
case "1y":
|
||||
duration = 60*60*24*365;
|
||||
resolution = 86400;
|
||||
break;
|
||||
}
|
||||
let timestamp_now = Math.round((new Date()).getTime() / 1000);
|
||||
let duration = parseInt($("#total_time_select > option:selected").data('duration'));
|
||||
let resolution = parseInt($("#total_time_select > option:selected").data('resolution'));
|
||||
// always round from timestamp to nearest hour
|
||||
const from_timestamp = Math.floor((timestamp_now -duration) / 3600 ) * 3600;
|
||||
return {resolution: resolution, from: from_timestamp, to: timestamp_now};
|
||||
@ -217,18 +165,10 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
let chart_data = [];
|
||||
data.map(function(item){
|
||||
let item_dir = item.key.split(',').pop();
|
||||
let item_intf = item.key.split(',')[0];
|
||||
if (item_intf != '0' && item_intf != 'lo0' ) {
|
||||
if (direction == item_dir) {
|
||||
if (interface_names[item_intf] != undefined) {
|
||||
item.key = interface_names[item_intf];
|
||||
} else {
|
||||
item.key = item_intf;
|
||||
}
|
||||
if (direction == item.direction) {
|
||||
item.key = item.interface ?? '-';
|
||||
chart_data.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart_data.sort(function(a, b) {
|
||||
@ -273,19 +213,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
let chart_data = [];
|
||||
data.map(function(item){
|
||||
var label = "(other)";
|
||||
var proto = "";
|
||||
if (item.protocol != "") {
|
||||
if (item.protocol in protocol_names) {
|
||||
proto = ' (' + protocol_names[item.protocol] + ')';
|
||||
}
|
||||
if (item.dst_port in service_names) {
|
||||
label = service_names[item.dst_port];
|
||||
} else {
|
||||
label = item.dst_port
|
||||
}
|
||||
}
|
||||
chart_data.push({'label': label + proto, 'value': item.total});
|
||||
chart_data.push({'label': item.label, 'value': item.total});
|
||||
});
|
||||
|
||||
var diag = d3.select("#chart_top_ports svg")
|
||||
@ -486,51 +414,24 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
});
|
||||
// dump rows
|
||||
data.map(function(item){
|
||||
let proto = '';
|
||||
if (item.protocol in protocol_names) {
|
||||
proto = ' (' + protocol_names[item.protocol] + ')';
|
||||
}
|
||||
let service_port;
|
||||
if (item.service_port in service_names) {
|
||||
service_port = service_names[item.service_port];
|
||||
} else {
|
||||
service_port = item.service_port
|
||||
}
|
||||
let tr_str = '<tr>';
|
||||
if (service_port != "") {
|
||||
tr_str += '<td> <span data-toggle="tooltip" title="'+proto+'/'+item.service_port+'">'+service_port+' </span> '+proto+'</td>';
|
||||
} else {
|
||||
tr_str += "<td>{{ lang._('(other)') }}</td>";
|
||||
}
|
||||
tr_str += '<td>' + item['src_addr'] + '</td>';
|
||||
tr_str += '<td>' + item['dst_addr'] + '</td>';
|
||||
tr_str += '<td>' + byteFormat(item['total']) + ' ' + '</td>';
|
||||
if (item['last_seen'] != "") {
|
||||
tr_str += '<td>' + d3.time.format('%b %e %H:%M:%S')(new Date(item['last_seen']*1000)) + '</td>';
|
||||
} else {
|
||||
tr_str += '<td></td>'
|
||||
}
|
||||
|
||||
|
||||
let percentage = parseInt((item['total'] /grand_total) * 100);
|
||||
let perc_text = ((item['total'] /grand_total) * 100).toFixed(2);
|
||||
tr_str += '<td>';
|
||||
tr_str += '<div class="progress-bar progress-bar-warning progress-bar-striped" role="progressbar" ';
|
||||
tr_str += 'aria-valuenow="'+percentage+'" aria-valuemin="0" aria-valuemax="100" style="color: black; min-width: 2em; width:' ;
|
||||
tr_str += percentage+'%;">'+perc_text+' %</div>';
|
||||
tr_str += '</td>';
|
||||
tr_str += '</tr>';
|
||||
html.push(tr_str);
|
||||
let percentage = parseInt((item.total /grand_total) * 100);
|
||||
let perc_text = ((item.total /grand_total) * 100).toFixed(2);
|
||||
html.push($("<tr/>").append([
|
||||
$("<td/>").text(item.label),
|
||||
$("<td/>").text(item.src_addr),
|
||||
$("<td/>").text(item.dst_addr),
|
||||
$("<td/>").text(byteFormat(item.total)),
|
||||
$("<td/>").text(item.last_seen_str),
|
||||
$("<td>").html(
|
||||
'<div class="progress-bar progress-bar-warning progress-bar-striped" role="progressbar" aria-valuenow="'+
|
||||
percentage+
|
||||
'" aria-valuemin="0" aria-valuemax="100" style="color: black; min-width: 2em; width:'+
|
||||
percentage+'%;">'+perc_text+' %'
|
||||
)
|
||||
]));
|
||||
});
|
||||
$("#netflow_details > tbody").html(html.join(''));
|
||||
if (grand_total > 0) {
|
||||
$("#netflow_details > tbody").empty().append(html);
|
||||
$("#netflow_details_total").html(byteFormat(grand_total));
|
||||
} else {
|
||||
$("#netflow_details_total").html("");
|
||||
}
|
||||
|
||||
// link tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -596,15 +497,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
// trigger initial tab load
|
||||
get_metadata().done(function(){
|
||||
// known interfaces
|
||||
for (var key in interface_names) {
|
||||
$('#interface_select').append($("<option></option>").attr("value",key).text(interface_names[key]));
|
||||
$('#interface_select_detail').append($("<option></option>").attr("value",key).text(interface_names[key]));
|
||||
}
|
||||
$('#interface_select').selectpicker('refresh');
|
||||
$('#interface_select_detail').selectpicker('refresh');
|
||||
|
||||
do_startup().done(function(){
|
||||
// generate date selection (utc start, end times)
|
||||
var now = new Date;
|
||||
var date_begin = Date.UTC(now.getUTCFullYear(),now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0);
|
||||
@ -670,16 +563,16 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
<div id="totals" class="tab-pane fade in active">
|
||||
<div class="pull-right">
|
||||
<select class="selectpicker" id="total_time_select">
|
||||
<option value="2h">{{ lang._('Last 2 hours, 30 second average') }}</option>
|
||||
<option value="8h">{{ lang._('Last 8 hours, 5 minute average') }}</option>
|
||||
<option value="24h">{{ lang._('Last 24 hours, 5 minute average') }}</option>
|
||||
<option value="7d">{{ lang._('7 days, 1 hour average') }}</option>
|
||||
<option value="14d">{{ lang._('14 days, 1 hour average') }}</option>
|
||||
<option value="30d">{{ lang._('30 days, 24 hour average') }}</option>
|
||||
<option value="60d">{{ lang._('60 days, 24 hour average') }}</option>
|
||||
<option value="90d">{{ lang._('90 days, 24 hour average') }}</option>
|
||||
<option value="182d">{{ lang._('182 days, 24 hour average') }}</option>
|
||||
<option value="1y">{{ lang._('Last year, 24 hour average') }}</option>
|
||||
<option data-duration="7200" data-resolution="30" value="2h">{{ lang._('Last 2 hours, 30 second average') }}</option>
|
||||
<option data-duration="28800" data-resolution="300" value="8h">{{ lang._('Last 8 hours, 5 minute average') }}</option>
|
||||
<option data-duration="86400" data-resolution="300" value="24h">{{ lang._('Last 24 hours, 5 minute average') }}</option>
|
||||
<option data-duration="604800" data-resolution="3600" value="7d">{{ lang._('7 days, 1 hour average') }}</option>
|
||||
<option data-duration="1209600" data-resolution="3600" value="14d">{{ lang._('14 days, 1 hour average') }}</option>
|
||||
<option data-duration="2592000" data-resolution="86400" value="30d">{{ lang._('30 days, 24 hour average') }}</option>
|
||||
<option data-duration="5184000" data-resolution="86400" value="60d">{{ lang._('60 days, 24 hour average') }}</option>
|
||||
<option data-duration="7776000" data-resolution="86400" value="90d">{{ lang._('90 days, 24 hour average') }}</option>
|
||||
<option data-duration="15724800" data-resolution="86400" value="182d">{{ lang._('182 days, 24 hour average') }}</option>
|
||||
<option data-duration="31536000" data-resolution="86400" value="1y">{{ lang._('Last year, 24 hour average') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
@ -73,7 +73,18 @@
|
||||
'get':'/api/dnsmasq/settings/get_' + grid_id + '/',
|
||||
'set':'/api/dnsmasq/settings/set_' + grid_id + '/',
|
||||
'add':'/api/dnsmasq/settings/add_' + grid_id + '/',
|
||||
'del':'/api/dnsmasq/settings/del_' + grid_id + '/'
|
||||
'del':'/api/dnsmasq/settings/del_' + grid_id + '/',
|
||||
options: {
|
||||
triggerEditFor: getUrlHash('edit'),
|
||||
initialSearchPhrase: getUrlHash('search'),
|
||||
requestHandler: function(request) {
|
||||
const selectedTags = $('#tag_select').val();
|
||||
if (selectedTags && selectedTags.length > 0) {
|
||||
request['tags'] = selectedTags;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
});
|
||||
/* insert headers when multiple grids exist on a single tab */
|
||||
let header = $("#" + grid_id + "-header");
|
||||
@ -90,6 +101,16 @@
|
||||
}
|
||||
} else {
|
||||
all_grids[grid_id].bootgrid('reload');
|
||||
|
||||
}
|
||||
// insert tag selectpicker in all grids that use tags or interfaces, boot excluded cause two grids in same tab
|
||||
if (!['domain', 'boot'].includes(grid_id)) {
|
||||
let header = $("#" + grid_id + "-header");
|
||||
let $actionBar = header.find('.actionBar');
|
||||
if ($actionBar.length) {
|
||||
$('#tag_select_container').detach().insertBefore($actionBar.find('.search'));
|
||||
$('#tag_select_container').show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -150,6 +171,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Populate tag selectpicker
|
||||
$('#tag_select').fetch_options('/api/dnsmasq/settings/get_tag_list');
|
||||
|
||||
$('#tag_select').change(function () {
|
||||
Object.keys(all_grids).forEach(function (grid_id) {
|
||||
// boot is not excluded here, as it reloads in same tab as options
|
||||
if (!['domain'].includes(grid_id)) {
|
||||
all_grids[grid_id].bootgrid('reload');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -157,6 +190,9 @@
|
||||
tbody.collapsible > tr > td:first-child {
|
||||
padding-left: 30px;
|
||||
}
|
||||
#tag_select_container {
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: none;" id="hosts_tfoot_append">
|
||||
@ -172,6 +208,11 @@
|
||||
<button id="download_hosts" type="button" title="{{ lang._('Export as csv') }}" data-toggle="tooltip" class="btn btn-xs"><span class="fa fa-fw fa-table"></span></button>
|
||||
</div>
|
||||
|
||||
<div id="tag_select_container" class="btn-group" style="display: none;">
|
||||
<select id="tag_select" class="selectpicker" multiple data-title="{{ lang._('Tags & Interfaces') }}" data-show-subtext="true" data-live-search="true" data-size="15" data-width="200px" data-container="body">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Navigation bar -->
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li><a data-toggle="tab" href="#general">{{ lang._('General') }}</a></li>
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
del:'/api/firewall/filter/del_rule/',
|
||||
toggle:'/api/firewall/filter/toggle_rule/',
|
||||
options: {
|
||||
resizableColumns: true,
|
||||
triggerEditFor: getUrlHash('edit'),
|
||||
initialSearchPhrase: getUrlHash('search'),
|
||||
rowCount: [20,50,100,200,500,1000],
|
||||
@ -500,38 +501,8 @@
|
||||
});
|
||||
|
||||
// Populate interface selectpicker
|
||||
$('#interface_select').fetch_options('/api/firewall/filter/get_interface_list');
|
||||
$("#interface_select_container").show();
|
||||
ajaxCall('/api/firewall/filter/get_interface_list', {},
|
||||
function(data, status) {
|
||||
const $select = $('#interface_select');
|
||||
$select.empty();
|
||||
for (const [groupkey, group] of Object.entries(data)) {
|
||||
if (group.items.length > 0) {
|
||||
let $optgroup = $('<optgroup>', {
|
||||
"label": `${group.label}`,
|
||||
"data-icon": `${group.icon}`
|
||||
});
|
||||
group.items.forEach(function(iface) {
|
||||
let optprops = {
|
||||
value: iface.value,
|
||||
'data-subtext': group.label,
|
||||
text: iface.label
|
||||
};
|
||||
if (iface.value === '') {
|
||||
/* floating selected by default */
|
||||
optprops['selected'] = 'selected';
|
||||
}
|
||||
$optgroup.append($('<option>', optprops));
|
||||
});
|
||||
$select.append($optgroup);
|
||||
}
|
||||
}
|
||||
$select.selectpicker('refresh');
|
||||
},
|
||||
function(xhr, textStatus, errorThrown) {
|
||||
console.error("Failed to load interface list:", textStatus, errorThrown);
|
||||
}
|
||||
);
|
||||
|
||||
// move selectpickers into action bar
|
||||
$("#interface_select_container").detach().insertBefore('#{{formGridFilterRule["table_id"]}}-header > .row > .actionBar > .search');
|
||||
|
||||
40
src/opnsense/scripts/firmware/cleanup.sh
Executable file
40
src/opnsense/scripts/firmware/cleanup.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (C) 2025 Franco Fichtner <franco@opnsense.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
REQUEST="CLEANUP"
|
||||
|
||||
. /usr/local/opnsense/scripts/firmware/config.sh
|
||||
|
||||
output_txt ">>> Removing unused packages from system"
|
||||
output_cmd ${PKG} autoremove -y
|
||||
|
||||
output_txt ">>> Removing stale update files from system"
|
||||
output_cmd opnsense-update -F
|
||||
|
||||
output_txt ">>> Removing package files from cache"
|
||||
output_cmd ${PKG} clean -ya
|
||||
|
||||
output_done
|
||||
@ -41,6 +41,7 @@ COMMANDS="
|
||||
bogons
|
||||
changelog
|
||||
check
|
||||
cleanup
|
||||
connection
|
||||
details
|
||||
health
|
||||
|
||||
@ -127,10 +127,12 @@ class Main(object):
|
||||
def set_config(cls, config):
|
||||
cls.config = config
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, single_pass=False):
|
||||
""" construct, hook signal handler and run aggregators
|
||||
:param single_pass: exit after one pass
|
||||
:return: None
|
||||
"""
|
||||
self.single_pass = single_pass
|
||||
self.running = True
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
self.run()
|
||||
@ -168,7 +170,7 @@ class Main(object):
|
||||
check_rotate(self.config.flowd_source)
|
||||
|
||||
# wait for next pass, exit on sigterm
|
||||
if Main.config.single_pass:
|
||||
if self.single_pass:
|
||||
break
|
||||
else:
|
||||
# calculate time to wait in between parses. since tailing flowd.log is quite time consuming
|
||||
@ -195,6 +197,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--console', dest='console', help='run in console', action='store_true')
|
||||
parser.add_argument('--profile', dest='profile', help='enable profiler', action='store_true')
|
||||
parser.add_argument('--repair', dest='repair', help='init repair', action='store_true')
|
||||
parser.add_argument('--single_pass', dest='single_pass', help='exit after first pass', action='store_true')
|
||||
cmd_args = parser.parse_args()
|
||||
|
||||
Main.set_config(load_config())
|
||||
@ -210,7 +213,7 @@ if __name__ == '__main__':
|
||||
|
||||
pr = cProfile.Profile(builtins=False)
|
||||
pr.enable()
|
||||
Main()
|
||||
Main(True)
|
||||
pr.disable()
|
||||
s = io.StringIO()
|
||||
sortby = 'cumulative'
|
||||
@ -218,7 +221,7 @@ if __name__ == '__main__':
|
||||
ps.print_stats()
|
||||
print (s.getvalue())
|
||||
else:
|
||||
Main()
|
||||
Main(cmd_args.single_pass)
|
||||
elif cmd_args.repair:
|
||||
# force a database repair, when
|
||||
try:
|
||||
|
||||
@ -44,7 +44,6 @@ class Config(object):
|
||||
pid_filename = '/var/run/flowd_aggregate.pid'
|
||||
flowd_source = '/var/log/flowd.log'
|
||||
database_dir = '/var/netflow'
|
||||
single_pass = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key in kwargs:
|
||||
|
||||
@ -27,9 +27,7 @@
|
||||
parse flowd log files
|
||||
"""
|
||||
import glob
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
from lib.flowparser import FlowParser
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (c) 2015-2022 Franco Fichtner <franco@opnsense.org>
|
||||
# Copyright (c) 2015-2025 Franco Fichtner <franco@opnsense.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
@ -100,6 +100,10 @@ ${RELEASE:-y})
|
||||
run_action connection
|
||||
exit 0
|
||||
;;
|
||||
[fF])
|
||||
run_action cleanup
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
|
||||
@ -50,7 +50,7 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
|
||||
def get_blocklist(self):
|
||||
result = {}
|
||||
for blocklist, bl_shortcode in self._blocklists_in_config():
|
||||
per_file_stats = {'uri': blocklist, 'skip': 0, 'blocklist': 0, 'wildcard': 0}
|
||||
per_file_stats = {'uri': blocklist, 'blocklist': 0, 'wildcard': 0}
|
||||
for domain in self._domains_in_blocklist(blocklist):
|
||||
if self.domain_pattern.match(domain):
|
||||
per_file_stats['blocklist'] += 1
|
||||
@ -66,11 +66,9 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
|
||||
per_file_stats['wildcard'] += 1
|
||||
else:
|
||||
result[domain] = {'bl': bl_shortcode, 'wildcard': False}
|
||||
else:
|
||||
per_file_stats['skip'] += 1
|
||||
syslog.syslog(
|
||||
syslog.LOG_NOTICE,
|
||||
'blocklist: %(uri)s (exclude: %(skip)d block: %(blocklist)d wildcard: %(wildcard)d)' % per_file_stats
|
||||
'blocklist: %(uri)s (block: %(blocklist)d wildcard: %(wildcard)d)' % per_file_stats
|
||||
)
|
||||
|
||||
if self.cnf and self.cnf.has_section('include'):
|
||||
|
||||
@ -102,6 +102,12 @@ parameters:
|
||||
type:script
|
||||
message:Retrieve health status
|
||||
|
||||
[cleanup]
|
||||
command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/firmware/launcher.sh cleanup
|
||||
parameters:
|
||||
type:script
|
||||
message:Run temporary file cleanup
|
||||
|
||||
[connection]
|
||||
command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/firmware/launcher.sh connection
|
||||
parameters:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
[reload]
|
||||
command:/etc/rc.d/dnctl start || true
|
||||
command:/etc/rc.d/dnctl start; exit 0
|
||||
parameters:
|
||||
type:script
|
||||
message:restarting dummynet
|
||||
|
||||
@ -59,7 +59,7 @@ bind-interfaces
|
||||
{% endif %}
|
||||
|
||||
{% if dnsmasq.add_mac %}
|
||||
add-mac{% if dnsmasq.add_mac != 'default' %}={{dnsmasq.add_mac}}{% endif %}
|
||||
add-mac{% if dnsmasq.add_mac != 'standard' %}={{dnsmasq.add_mac}}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if dnsmasq.add_subnet %}
|
||||
@ -186,9 +186,32 @@ ra-param={{ helpers.physical_interface(dhcp_range.interface) }}
|
||||
{% endfor %}
|
||||
|
||||
{% for host in helpers.toList('dnsmasq.hosts') %}
|
||||
{% if host.hwaddr and host.hwaddr.find(':') == -1%}
|
||||
dhcp-host={{host.hwaddr}}{% if host.set_tag%},set:{{host.set_tag|replace('-','')}}{% endif %},{{host.ip}}
|
||||
{# Skip if MAC is missing or invalid (no colon), unless client_id is present #}
|
||||
{% if not host.client_id and (not host.hwaddr or host.hwaddr.find(':') == -1) %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
dhcp-host=
|
||||
{%- if host.client_id -%}
|
||||
id:{{ host.client_id }},
|
||||
{%- endif -%}
|
||||
{%- if host.hwaddr -%}
|
||||
{{ host.hwaddr }},
|
||||
{%- endif -%}
|
||||
{%- if host.set_tag -%}
|
||||
set:{{ host.set_tag|replace('-', '') }},
|
||||
{%- endif -%}
|
||||
{%- for ip in host.ip.split(',')|map('trim') if host.ip -%}
|
||||
{{ '[' ~ ip ~ ']' if ':' in ip else ip }}{%- if not loop.last %},{% endif %}
|
||||
{%- endfor -%}
|
||||
{%- if host.host -%}
|
||||
,{{ host.host }}
|
||||
{%- endif -%}
|
||||
{%- if host.lease_time -%}
|
||||
,{{ host.lease_time }}{% else %},86400
|
||||
{%- endif -%}
|
||||
{%- if host.ignore|default('0') == '1' -%}
|
||||
,ignore
|
||||
{%- endif +%}
|
||||
{% endfor %}
|
||||
|
||||
{% set has_default=[] %}
|
||||
|
||||
46
src/opnsense/site-python/params.py
Normal file
46
src/opnsense/site-python/params.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""
|
||||
Copyright (c) 2015-2016 Ad Schellevis <ad@opnsense.org>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def update_params(parameters):
|
||||
""" update predefined parameters with given list from shell (as switches)
|
||||
for example /a valA /b valB
|
||||
converts to
|
||||
{'a':'valA','b':'valB'}
|
||||
(assuming parameters contains both a and b)
|
||||
:param parameters: parameter dictionary
|
||||
:return:
|
||||
"""
|
||||
cmd = None
|
||||
for arg in sys.argv[1:]:
|
||||
if cmd is None:
|
||||
cmd = arg[1:]
|
||||
else:
|
||||
if cmd in parameters and arg.strip() != '':
|
||||
parameters[cmd] = arg.strip()
|
||||
cmd = None
|
||||
@ -74,7 +74,8 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootgrid-table {
|
||||
table-layout: fixed;
|
||||
table-layout: auto;
|
||||
border-collapse: separate;
|
||||
}
|
||||
.bootgrid-table a {
|
||||
outline: 0;
|
||||
@ -165,3 +166,33 @@
|
||||
text-overflow: inherit !important;
|
||||
white-space: inherit !important;
|
||||
}
|
||||
.bootgrid-rc-container {
|
||||
position: relative;
|
||||
overflow: none;
|
||||
}
|
||||
.bootgrid-rc-handle {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
cursor: ew-resize;
|
||||
margin-left: -3px;
|
||||
z-index: 2;
|
||||
}
|
||||
table.bootgrid-rc-resizing {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.bootgrid-rc-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.highlight,
|
||||
.highlight-move {
|
||||
border-right: 1px solid rgba(128, 128, 128, 1);
|
||||
}
|
||||
.no-hover * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -84,6 +84,7 @@ function init()
|
||||
renderTableHeader.call(this);
|
||||
renderSearchField.call(this);
|
||||
renderActions.call(this);
|
||||
initializeResizableColumns.call(this);
|
||||
loadData.call(this);
|
||||
|
||||
this.element.trigger("initialized" + namespace);
|
||||
@ -140,9 +141,11 @@ function loadColumns()
|
||||
setStaged: false,
|
||||
unsetStaged: false,
|
||||
width: ($.isNumeric(data.width)) ? data.width + "px" :
|
||||
(typeof(data.width) === "string") ? data.width : null
|
||||
(typeof(data.width) === "string") ? data.width : null,
|
||||
calculatedWidth: null
|
||||
};
|
||||
that.columns.push(column);
|
||||
that.columnMap.set(column.id, column);
|
||||
if (column.order != null)
|
||||
{
|
||||
that.sortDictionary[column.id] = column.order;
|
||||
@ -205,9 +208,15 @@ function update(rows, total)
|
||||
if (shouldRenderHeader) {
|
||||
/* multiple columns have been set/unset prior to this reload */
|
||||
renderTableHeader.call(that);
|
||||
resetHandleOverlay.call(that);
|
||||
}
|
||||
|
||||
renderRows.call(that, rows);
|
||||
|
||||
window.setTimeout(function () {
|
||||
syncHandlePositions.call(that);
|
||||
}, 10);
|
||||
|
||||
renderInfos.call(that);
|
||||
renderPagination.call(that);
|
||||
|
||||
@ -272,7 +281,6 @@ function loadData(soft=false)
|
||||
that.current = response.current;
|
||||
that.cachedResponse = response;
|
||||
update.call(that, response.rows, response.total);
|
||||
this.noResultsRendered = false;
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown)
|
||||
{
|
||||
@ -414,6 +422,7 @@ function renderActions()
|
||||
e.stopPropagation();
|
||||
Object.keys(localStorage)
|
||||
.filter(key =>
|
||||
key.startsWith(`columnSizes[${that.uid}`) ||
|
||||
key.startsWith(`visibleColumns[${that.uid}`) ||
|
||||
key.startsWith(`rowCount[${that.uid}`) ||
|
||||
key.startsWith(`sortColumns[${that.uid}`)
|
||||
@ -458,12 +467,12 @@ function renderColumnSelection(actions)
|
||||
if (!checkbox.prop("disabled"))
|
||||
{
|
||||
column.visible = localStorage.getItem('visibleColumns[' + that.uid + '][' + column.id + ']') === 'true';
|
||||
column.visible ? column.setStaged = true : column.unsetStaged = true;
|
||||
var enable = that.columns.where(isVisible).length > 1;
|
||||
$this.parents(itemsSelector).find(selector + ":has(" + checkboxSelector + ":checked)")
|
||||
._bgEnableAria(enable).find(checkboxSelector)._bgEnableField(enable);
|
||||
|
||||
that.element.find("tbody").empty(); // Fixes an column visualization bug
|
||||
renderTableHeader.call(that);
|
||||
that.columnSelectForceReload ? loadData.call(that)
|
||||
: update.call(that, that.cachedResponse.rows, that.cachedResponse.total);
|
||||
}
|
||||
@ -1040,6 +1049,302 @@ function sortRows()
|
||||
}
|
||||
}
|
||||
|
||||
// RESIZABLE COLUMN INTERNAL FUNCTIONS
|
||||
// ====================
|
||||
|
||||
function initializeResizableColumns() {
|
||||
if (!this.options.resizableColumns) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetHandleOverlay.call(this);
|
||||
|
||||
syncHandlePositions.call(this);
|
||||
|
||||
bindEvents($(window), ['resize'], null, syncHandlePositions.bind(this));
|
||||
}
|
||||
|
||||
function resetHandleOverlay() {
|
||||
let that = this;
|
||||
|
||||
if (!this.options.resizableColumns) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.columns.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$tableHeaders = this.element.find(`tr:first > th:visible${this.options.rowSelect ? ':gt(0)' : ''}`);
|
||||
|
||||
// annotate initial column width
|
||||
this.$tableHeaders.each(function (i, el) {
|
||||
let $el = $(el);
|
||||
let col = that.columnMap.get($el.data('column-id'));
|
||||
let pxWidth = 0;
|
||||
|
||||
if (!col || col.width == null) {
|
||||
pxWidth = $el.outerWidth();
|
||||
} else {
|
||||
pxWidth = toPixels(col.width);
|
||||
$el.data('minWidth', pxWidth);
|
||||
}
|
||||
|
||||
if (col) {
|
||||
let stored = localStorage.getItem(`columnSizes[${that.uid}][${col.id}]`);
|
||||
if (stored != null) {
|
||||
setWidth.call(that, el, parseFloat(stored));
|
||||
} else if (col.calculatedWidth != null) {
|
||||
setWidth.call(that, el, col.calculatedWidth);
|
||||
} else {
|
||||
setWidth.call(that, el, pxWidth);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// setup handle container overlay
|
||||
if (this.$resizableHandleContainer != null) {
|
||||
this.$resizableHandleContainer.remove();
|
||||
}
|
||||
|
||||
this.$resizableHandleContainer = $(`<div class="bootgrid-rc-container"/>`);
|
||||
this.element.before(this.$resizableHandleContainer);
|
||||
|
||||
this.$tableHeaders.each(function (i, el) {
|
||||
let $current = that.$tableHeaders.eq(i);
|
||||
let $next = that.$tableHeaders.eq(i + 1);
|
||||
|
||||
if ($next.length === 0 || $current.is('[data-noresize]') || $next.is('[data-noresize]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(`<div class="bootgrid-rc-handle"/>`).data('th', $(el)).data('id', $(el).data('column-id'))
|
||||
.hover(
|
||||
function() {
|
||||
$(el).addClass('highlight');
|
||||
that.element.find('tr').map(function() {
|
||||
$(this).find('td').eq($(el).index()).addClass('highlight');
|
||||
});
|
||||
},
|
||||
function() {
|
||||
$(el).removeClass('highlight');
|
||||
that.element.find('tr').map(function() {
|
||||
$(this).find('td').eq($(el).index()).removeClass('highlight');
|
||||
});
|
||||
}
|
||||
)
|
||||
.appendTo(that.$resizableHandleContainer);
|
||||
});
|
||||
|
||||
bindEvents(
|
||||
this.$resizableHandleContainer,
|
||||
['mousedown', 'touchstart'],
|
||||
'.bootgrid-rc-handle',
|
||||
onMouseDown.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
function syncHandlePositions() {
|
||||
let that = this;
|
||||
|
||||
if (!this.options.resizableColumns) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sync handle container to table width
|
||||
this.$resizableHandleContainer.width(this.element.width());
|
||||
|
||||
// sync individual handles
|
||||
this.$resizableHandleContainer.find('.bootgrid-rc-handle').each(function (i, el) {
|
||||
let $el = $(el);
|
||||
let $originalHeader = $el.data('th');
|
||||
|
||||
var left = $originalHeader.outerWidth() + ($originalHeader.offset().left - that.$resizableHandleContainer.offset().left);
|
||||
$el.css({ left: left, height: that.element.find('thead').height() })
|
||||
});
|
||||
}
|
||||
|
||||
function onMouseDown(event) {
|
||||
let $currentHandle = $(event.currentTarget);
|
||||
let idx = $currentHandle.index();
|
||||
let $leftCol = this.$tableHeaders.eq(idx).not('[data-noresize]');
|
||||
let leftCol = this.columnMap.get($leftCol.data('column-id'));
|
||||
let $rightCol = this.$tableHeaders.eq(idx + 1).not('[data-noresize]');
|
||||
let rightCol = this.columnMap.get($rightCol.data('column-id'));
|
||||
|
||||
if ($currentHandle.is('[data-noresize]') || !leftCol || !rightCol) {
|
||||
return;
|
||||
}
|
||||
|
||||
let leftColWidth = leftCol.calculatedWidth;
|
||||
let rightColWidth = rightCol.calculatedWidth;
|
||||
|
||||
if (this.resizeOp) {
|
||||
onMouseUp.call(this, event);
|
||||
}
|
||||
|
||||
// start operation
|
||||
this.resizeOp = {
|
||||
$leftCol: $leftCol,
|
||||
$rightCol: $rightCol,
|
||||
$currentHandle, $currentHandle,
|
||||
|
||||
startX: getPointerX(event),
|
||||
|
||||
widths: {
|
||||
left: leftColWidth,
|
||||
right: rightColWidth
|
||||
},
|
||||
newWidths: {
|
||||
left: leftColWidth,
|
||||
right: rightColWidth
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents(
|
||||
$(this.element[0].ownerDocument),
|
||||
['mousemove', 'touchmove'],
|
||||
null,
|
||||
onMouseMove.bind(this)
|
||||
);
|
||||
|
||||
bindEvents(
|
||||
$(this.element[0].ownerDocument),
|
||||
['mouseup', 'touchend'],
|
||||
null,
|
||||
onMouseUp.bind(this)
|
||||
);
|
||||
|
||||
this.$resizableHandleContainer.add(this.element).addClass('bootgrid-rc-resizing');
|
||||
$leftCol.add($rightCol).addClass('bootgrid-rc-resizing');
|
||||
|
||||
$leftCol.addClass('highlight-move');
|
||||
this.element.find('tr').map(function() {
|
||||
let i = $currentHandle.data('th').index();
|
||||
$(this).find('td').eq(i).addClass('highlight-move');
|
||||
});
|
||||
|
||||
// prevent global hover effects when resizing
|
||||
$('body').addClass('no-hover');
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
let op = this.resizeOp
|
||||
if (!this.resizeOp) {
|
||||
return;
|
||||
}
|
||||
|
||||
let diff = (getPointerX(event) - op.startX)
|
||||
if (diff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let leftCol = op.$leftCol[0];
|
||||
let rightCol = op.$rightCol[0];
|
||||
let widthLeft = undefined;
|
||||
let widthRight = undefined;
|
||||
|
||||
if (diff > 0) {
|
||||
widthLeft = constrainWidth.call(this, op.widths.left + (op.widths.right - op.newWidths.right), op.$leftCol);
|
||||
widthRight = constrainWidth.call(this, op.widths.right - diff, op.$rightCol);
|
||||
} else if (diff < 0) {
|
||||
widthLeft = constrainWidth.call(this, op.widths.left + diff, op.$leftCol);
|
||||
widthRight = constrainWidth.call(this, op.widths.right + (op.widths.left - op.newWidths.left), op.$rightCol);
|
||||
}
|
||||
|
||||
if (leftCol && widthRight >= 0) {
|
||||
setWidth.call(this, leftCol, widthLeft);
|
||||
}
|
||||
if (rightCol && widthLeft >= 0) {
|
||||
setWidth.call(this, rightCol, widthRight);
|
||||
}
|
||||
|
||||
op.newWidths.left = widthLeft;
|
||||
op.newWidths.right = widthRight;
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
let that = this;
|
||||
let idx = this.resizeOp.$currentHandle.index();
|
||||
|
||||
if (!this.resizeOp) {
|
||||
return;
|
||||
}
|
||||
|
||||
unbindEvents($(this.element[0].ownerDocument), ['mouseup', 'touchend', 'mousemove', 'touchmove']);
|
||||
|
||||
this.$resizableHandleContainer.add(this.element).removeClass('bootgrid-rc-resizing');
|
||||
|
||||
this.resizeOp.$leftCol.add(this.resizeOp.$rightCol).add(this.resizeOp.$currentHandle).removeClass('bootgrid-rc-resizing');
|
||||
|
||||
this.resizeOp.$leftCol.removeClass('highlight-move');
|
||||
this.element.find('tr').map(function() {
|
||||
let i = that.resizeOp.$currentHandle.data('th').index();
|
||||
$(this).find('td').eq(i).removeClass('highlight-move');
|
||||
});
|
||||
$('body').removeClass('no-hover');
|
||||
|
||||
this.columns.where(isVisible).forEach(function(col) {
|
||||
localStorage.setItem(`columnSizes[${that.uid}][${col.id}]`, col.calculatedWidth);
|
||||
})
|
||||
|
||||
syncHandlePositions.call(this);
|
||||
|
||||
this.resizeOp = null;
|
||||
}
|
||||
|
||||
function bindEvents($target, events, data, callback) {
|
||||
events = events.join(namespace + ' ') + namespace;
|
||||
$target.on(events, data, callback);
|
||||
}
|
||||
|
||||
function unbindEvents($target, events) {
|
||||
events = events.join(namespace + ' ') + namespace;
|
||||
$target.off(events);
|
||||
}
|
||||
|
||||
function getPointerX(event) {
|
||||
if (event.type.indexOf('touch') === 0) {
|
||||
return (event.originalEvent.touches[0] || event.originalEvent.changedTouches[0]).pageX;
|
||||
}
|
||||
return event.pageX;
|
||||
}
|
||||
|
||||
function constrainWidth(width, $el) {
|
||||
let options = this.options.resizableColumnSettings;
|
||||
let min = $el.data('minWidth');
|
||||
|
||||
if (options.minWidth != undefined) {
|
||||
width = Math.max(options.minWidth, width);
|
||||
}
|
||||
|
||||
if (min != undefined) {
|
||||
width = Math.max(min, width);
|
||||
}
|
||||
|
||||
if (options.maxWidth != undefined) {
|
||||
width = Math.min(options.maxWidth, width);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
function setWidth(element, width) {
|
||||
let col = this.columnMap.get($(element).data('column-id'));
|
||||
width = width > 0 ? width : 0;
|
||||
element.style.width = width + 'px';
|
||||
col.calculatedWidth = width;
|
||||
}
|
||||
|
||||
function toPixels(value) {
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.style.width = value;
|
||||
document.body.appendChild(tempElement);
|
||||
const pixels = window.getComputedStyle(tempElement).width;
|
||||
document.body.removeChild(tempElement);
|
||||
return parseFloat(pixels);
|
||||
}
|
||||
|
||||
// GRID PUBLIC CLASS DEFINITION
|
||||
// ====================
|
||||
|
||||
@ -1060,6 +1365,7 @@ var Grid = function(element, options)
|
||||
// overrides rowCount explicitly because deep copy ($.extend) leads to strange behaviour
|
||||
var rowCount = this.options.rowCount = this.element.data().rowCount || options.rowCount || this.options.rowCount;
|
||||
this.columns = [];
|
||||
this.columnMap = new Map();
|
||||
this.current = 1;
|
||||
this.currentRows = [];
|
||||
this.identifier = null; // The first column ID that is marked as identifier
|
||||
@ -1088,6 +1394,10 @@ var Grid = function(element, options)
|
||||
this.xqr = null;
|
||||
this.uid = window.location.pathname + "#" + this.element.attr('id');
|
||||
|
||||
this.$tableHeaders = [];
|
||||
this.$resizableHandleContainer = null;
|
||||
this.resizeOp = null;
|
||||
|
||||
// todo: implement cache
|
||||
};
|
||||
|
||||
@ -1225,6 +1535,13 @@ Grid.defaults = {
|
||||
method: "POST"
|
||||
},
|
||||
|
||||
resizableColumns: false,
|
||||
|
||||
resizableColumnSettings: {
|
||||
minWidth: 50,
|
||||
maxWidth: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Enriches the request object with additional properties. Either a `PlainObject` or a `Function`
|
||||
* that returns a `PlainObject` can be passed. Default value is `{}`.
|
||||
@ -1517,7 +1834,7 @@ Grid.defaults = {
|
||||
actionDropDownCheckboxItem: "<li><label class=\"{{css.dropDownItem}}\"><input id=\"{{ctx.id}}\" name=\"{{ctx.name}}\" type=\"checkbox\" value=\"1\" class=\"{{css.dropDownItemCheckbox}}\" {{ctx.checked}} /> {{ctx.label}}</label></li>",
|
||||
actions: "<div class=\"{{css.actions}}\"></div>",
|
||||
body: "<tbody></tbody>",
|
||||
cell: "<td class=\"{{ctx.css}}\" style=\"{{ctx.style}}\">{{ctx.content}}</td>",
|
||||
cell: "<td class=\"bootgrid-rc-column {{ctx.css}}\" style=\"{{ctx.style}}\">{{ctx.content}}</td>",
|
||||
footer: "<div id=\"{{ctx.id}}\" class=\"{{css.footer}}\"><div class=\"row\"><div class=\"col-sm-6\"><p class=\"{{css.pagination}}\"></p></div><div class=\"col-sm-6 infoBar\"><p class=\"{{css.infos}}\"></p></div></div></div>",
|
||||
header: "<div id=\"{{ctx.id}}\" class=\"{{css.header}}\"><div class=\"row\"><div class=\"col-sm-12 actionBar\"><p class=\"{{css.search}}\"></p><p class=\"{{css.actions}}\"></p></div></div></div>",
|
||||
headerCell: "<th data-column-id=\"{{ctx.column.id}}\" class=\"{{ctx.css}}\" style=\"{{ctx.style}}\"><a href=\"javascript:void(0);\" class=\"{{css.columnHeaderAnchor}} {{ctx.sortable}}\"><span class=\"{{css.columnHeaderText}}\">{{ctx.column.headerText}}</span>{{ctx.icon}}</a></th>",
|
||||
|
||||
@ -622,6 +622,75 @@ $.fn.SimpleActionButton = function (params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch option list for remote url, when a list is returned, expects either a list of items formatted like
|
||||
* [
|
||||
* {'label': 'my_label', value: 'my_value'}
|
||||
* ]
|
||||
* or an option group like:
|
||||
* {
|
||||
* group_value: {
|
||||
* label: 'my_group_label',
|
||||
* icon: 'fa fa-tag text-primary',
|
||||
* items: [{'label': 'my_label', value: 'my_value'}]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* When data is formatted differently, the data_callback can be used to re-format.
|
||||
*
|
||||
* @param url
|
||||
* @param params
|
||||
* @param data_callback callout to cleanse data before usage
|
||||
* @param store_data store data in data attribute (in its original form)
|
||||
*/
|
||||
$.fn.fetch_options = function(url, params, data_callback, store_data) {
|
||||
var deferred = $.Deferred();
|
||||
var $obj = $(this);
|
||||
$obj.empty();
|
||||
|
||||
ajaxGet(url, params ?? {}, function(data){
|
||||
if (store_data === true) {
|
||||
$obj.data("store", data);
|
||||
}
|
||||
if (typeof data_callback === "function") {
|
||||
data = data_callback(data);
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
data.map(function (item) {
|
||||
$obj.append($("<option/>").attr({"value": item.value}).text(item.label));
|
||||
});
|
||||
} else {
|
||||
for (const groupKey in data) {
|
||||
const group = data[groupKey];
|
||||
if (group.items.length > 0) {
|
||||
const $optgroup = $('<optgroup>', {
|
||||
label: group.label,
|
||||
'data-icon': group.icon
|
||||
});
|
||||
for (const item of group.items) {
|
||||
$optgroup.append(
|
||||
$('<option>', {
|
||||
value: item.value,
|
||||
text: item.label,
|
||||
'data-subtext': group.label,
|
||||
selected: item.selected ? 'selected' : undefined
|
||||
})
|
||||
);
|
||||
}
|
||||
$obj.append($optgroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($obj.hasClass('selectpicker')) {
|
||||
$obj.selectpicker('refresh');
|
||||
}
|
||||
$obj.change();
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* File upload dialog, constructs a modal, asks for a file to upload and sets {'payload': ..,, 'filename': ...}
|
||||
|
||||
@ -73,7 +73,8 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootgrid-table {
|
||||
table-layout: fixed;
|
||||
table-layout: auto;
|
||||
border-collapse: separate;
|
||||
}
|
||||
.bootgrid-table a {
|
||||
outline: 0;
|
||||
@ -163,3 +164,33 @@
|
||||
text-overflow: inherit !important;
|
||||
white-space: inherit !important;
|
||||
}
|
||||
.bootgrid-rc-container {
|
||||
position: relative;
|
||||
overflow: none;
|
||||
}
|
||||
.bootgrid-rc-handle {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
cursor: ew-resize;
|
||||
margin-left: -3px;
|
||||
z-index: 2;
|
||||
}
|
||||
table.bootgrid-rc-resizing {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.bootgrid-rc-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.highlight,
|
||||
.highlight-move {
|
||||
border-right: 1px solid rgba(128, 128, 128, 1);
|
||||
}
|
||||
.no-hover * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ $( document ).ready(function() {
|
||||
<option value="down" <?=$pconfig['trigger'] == "down" ? "selected=\"selected\"" :"";?> ><?=gettext("Member Down");?></option>
|
||||
<option value="downloss" <?=$pconfig['trigger'] == "downloss" ? "selected=\"selected\"" :"";?> ><?=gettext("Packet Loss");?></option>
|
||||
<option value="downlatency" <?=$pconfig['trigger'] == "downlatency" ? "selected=\"selected\"" :"";?> ><?=gettext("High Latency");?></option>
|
||||
<option value="downlosslatency" <?=$pconfig['trigger'] == "downlosslatency" ? "selected=\"selected\"" :"";?> ><?=gettext("Packet Loss or High Latency");?></option>
|
||||
<option value="downlosslatency" <?=$pconfig['trigger'] == "downlosslatency" ? "selected=\"selected\"" :"";?> ><?=gettext("Packet Loss and High Latency");?></option>
|
||||
</select>
|
||||
<div data-for="help_for_triggerlvl" class="hidden">
|
||||
<?=gettext("When to trigger exclusion of a member"); ?>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user