' . html_escape(__('Internal Host ↔ External Host. Click a device to see its individual connections. Bandwidth is averaged over the selected timespan.', 'flowview')) . '
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: ' . fv_nbar_retention_days() . ' days.
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
' . html_escape(__('Results are ordered by data volume; Connections shows up to 100 per page. Column sorting applies to the displayed page. NBAR history uses the configured retention setting. Daily summaries are reused across report selections; partial days use hourly summaries, and partial or changed hours use raw records. Connections pages are read from shared daily history, with nearby pages briefly cached for navigation. Average Mbps covers the selected period; allow 1–2 minutes for exports. Capture visibility depends on the device exporter and observation point. NAT can hide internal IPv4 addresses on a WAN capture. Select one observation point per traffic path to avoid duplicate totals. Cisco NBAR may classify encrypted traffic as SSL, QUIC or Unknown. History starts when export is enabled.', 'flowview')) . '
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: ' . fv_nbar_retention_days() . ' days.
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
' . html_escape(__('Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: %d days.', fv_nbar_retention_days(), 'flowview')) . '
';
echo '';
echo '';
if ($mode === 'conversations') {
- include __DIR__ . '/nbar_conversations.php';
+ include(__DIR__ . '/nbar_conversations.php');
bottom_footer();
exit;
}
@@ -286,9 +292,7 @@
$sql = 'SELECT d.app_id,n.name,SUM(d.bytes) bytes,SUM(d.packets) packets,SUM(d.flows) records,IF(COUNT(d.first_seen)=COUNT(*),MIN(d.first_seen),NULL) first_seen,IF(COUNT(d.last_seen)=COUNT(*),MAX(d.last_seen),NULL) last_seen FROM (SELECT d.listener_id,d.app_id,d.router,SUM(d.bytes) bytes,SUM(d.packets) packets,SUM(d.flows) flows,IF(COUNT(d.first_seen)=COUNT(*),MIN(d.first_seen),NULL) first_seen,IF(COUNT(d.last_seen)=COUNT(*),MAX(d.last_seen),NULL) last_seen FROM ' . $nbar_source . ' d WHERE ' . $where . ' GROUP BY d.listener_id,d.app_id,d.router) d JOIN plugin_flowview_devices l ON l.id=d.listener_id LEFT JOIN plugin_flowview_nbar_names n ON n.router=COALESCE(NULLIF(d.router,SPACE(0)),l.allowfrom) AND n.app_id=d.app_id GROUP BY d.app_id,n.name ORDER BY bytes DESC LIMIT 100';
}
if ($mode === 'connections') {
- require_once __DIR__ . '/nbar_count.php';
$nbar_count_key = fv_nbar_count_context('', $where, $args, $end, $lid === 0 ? $combined_lids : [$lid], $start);
- require_once __DIR__ . '/nbar_connections.php';
try {
$rows = fv_nbar_connections_cached($lid === 0 ? $combined_lids : [$lid], $start, $end, $where, $args, $page);
} catch (Throwable $e) {
diff --git a/phpunit.xml b/phpunit.xml
index 03f0a9bb..37f5c0e6 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -40,22 +40,22 @@
includes/functions.phpincludes/database.phpincludes/nbar_database.php
- nbar_chart.php
- nbar_collector.php
- nbar_common.php
- nbar_connections.php
+ includes/nbar_chart.php
+ includes/nbar_collector.php
+ includes/nbar_common.php
+ includes/nbar_connections.phpnbar_conversations.php
- nbar_count.php
- nbar_daily.php
- nbar_dns.php
- nbar_feed_status.php
- nbar_preferences.php
- nbar_query.php
+ includes/nbar_count.php
+ includes/nbar_daily.php
+ includes/nbar_dns.php
+ includes/nbar_feed_status.php
+ includes/nbar_preferences.php
+ includes/nbar_query.phpnbar_report.php
- nbar_seen.php
+ includes/nbar_seen.phpnbar_setup_guide.php
- nbar_summary.php
- nbar_timespan.php
+ includes/nbar_summary.php
+ includes/nbar_timespan.phpflowview_sections.php
diff --git a/setup.php b/setup.php
index 9a78d939..96956531 100644
--- a/setup.php
+++ b/setup.php
@@ -23,6 +23,8 @@
+-------------------------------------------------------------------------+
*/
+require_once(__DIR__ . '/includes/nbar_common.php');
+
/**
* Return the CSP nonce attribute for inline ';
- echo '';
-
+ print '' . html_escape(__('Choose two or three captures to combine, normally one LAN capture per router. Leave all captures unticked to save Internal Networks only. Overlapping LAN and WAN captures are added together and can count traffic twice.', 'flowview')) . '';
+ print '
' . html_escape(__('One internal IPv4 or IPv6 network per line. Used to identify internal hosts and upload/download direction in Conversations. Update delegated IPv6 prefixes when they change, and keep old prefixes for retained history. Exported flows do not automatically identify your site networks.', 'flowview')) . '
';
+ print '';
+ print '';
}
/** Exclude multicast endpoints from internal/external conversations. */
@@ -178,5 +196,6 @@ function fv_nbar_unicast_sql(string $field): string {
if (!in_array($field, ['d.src', 'd.dst'], true)) {
throw new InvalidArgumentException('Invalid address field');
}
+
return "NOT ((LENGTH($field)=4 AND (ORD(SUBSTRING($field,1,1)) & 240)=224) OR (LENGTH($field)=16 AND ORD(SUBSTRING($field,1,1))=255))";
}
diff --git a/includes/nbar_query.php b/includes/nbar_query.php
index bf9c0857..20189044 100644
--- a/includes/nbar_query.php
+++ b/includes/nbar_query.php
@@ -26,8 +26,8 @@
/**
* Fetch a bounded report result with a short account-session cache.
*
- * @param string $sql Trusted SQL statement.
- * @param array $args Prepared statement values.
+ * @param string $sql Trusted SQL statement.
+ * @param array $args Prepared statement values.
*
* @return array|false
*/
@@ -37,25 +37,30 @@ function fv_nbar_query_cached(string $sql, array $args): array|false {
}
// Fetch five adjacent pages together; retain one extra row for Next.
$slice = null;
+
if (preg_match('/ LIMIT 101 OFFSET ([0-9]+)(?=\)|$)/', $sql, $match)) {
$offset = (int) $match[1];
- $block = (int) (floor($offset / 500) * 500);
- $slice = $offset - $block;
- $sql = preg_replace('/ LIMIT 101 OFFSET [0-9]+(?=\)|$)/', ' LIMIT 501 OFFSET ' . $block, $sql);
+ $block = (int) (floor($offset / 500) * 500);
+ $slice = $offset - $block;
+ $sql = preg_replace('/ LIMIT 101 OFFSET [0-9]+(?=\)|$)/', ' LIMIT 501 OFFSET ' . $block, $sql);
}
- $now = time();
- $key = hash('sha256', serialize([$_SESSION['sess_user_id'] ?? 0, $sql, $args]));
+ $now = time();
+ $key = hash('sha256', serialize([$_SESSION['sess_user_id'] ?? 0, $sql, $args]));
$cache = $_SESSION['fv_nbar_query_cache'] ?? [];
+
foreach ($cache as $id => $entry) {
if ($entry['until'] <= $now) {
unset($cache[$id]);
}
}
+
if (isset($cache[$key])) {
$_SESSION['fv_nbar_query_cache'] = $cache;
+
return $slice === null ? $cache[$key]['rows'] : array_slice($cache[$key]['rows'], $slice, 101);
}
$rows = flowview_db_fetch_assoc_prepared($sql, $args);
+
// Cache successful, compact result sets only; never retain query failures.
if (is_array($rows) && strlen(serialize($rows)) <= 524288) {
while (cacti_sizeof($cache) >= 12) {
@@ -64,6 +69,7 @@ function fv_nbar_query_cached(string $sql, array $args): array|false {
$cache[$key] = ['until' => time() + ($slice === null ? 30 : 120), 'rows' => $rows];
}
$_SESSION['fv_nbar_query_cache'] = $cache;
+
return $slice === null || !is_array($rows) ? $rows : array_slice($rows, $slice, 101);
}
@@ -71,15 +77,16 @@ function fv_nbar_query_cached(string $sql, array $args): array|false {
* Build an exact grouped connection-page query over a trusted source.
*
* @param string $source Trusted summary or raw SQL source.
- * @param string $where Validated filter predicate.
- * @param int $page One-based connection page.
+ * @param string $where Validated filter predicate.
+ * @param int $page One-based connection page.
*
* @return string
*/
function fv_nbar_connections_sql(string $source, string $where, int $page): string {
// Use compact name ranks while grouping; resolve display names after paging.
- $map = '(SELECT router,app_id,DENSE_RANK() OVER (ORDER BY name) name_id FROM plugin_flowview_nbar_names)';
+ $map = '(SELECT router,app_id,DENSE_RANK() OVER (ORDER BY name) name_id FROM plugin_flowview_nbar_names)';
$group = 'd.app_id,COALESCE(n.name_id,0),d.src,d.dst,d.sport,d.dport,d.protocol,d.router';
$inner = 'SELECT d.router,d.app_id,COALESCE(n.name_id,0) name_id,SUM(d.bytes) bytes,SUM(d.packets) packets,SUM(d.flows) records,IF(COUNT(d.first_seen)=COUNT(*),MIN(d.first_seen),NULL) first_seen,IF(COUNT(d.last_seen)=COUNT(*),MAX(d.last_seen),NULL) last_seen,d.src,d.dst,d.sport,d.dport,d.protocol FROM ' . $source . ' d JOIN plugin_flowview_devices l ON l.id=d.listener_id LEFT JOIN ' . $map . ' n ON n.router=COALESCE(NULLIF(d.router,SPACE(0)),l.allowfrom) AND n.app_id=d.app_id WHERE ' . $where . ' GROUP BY ' . $group . ' ORDER BY bytes DESC,' . $group . ' LIMIT 101 OFFSET ' . (($page - 1) * 100);
+
return 'SELECT d.router,d.app_id,n.name,d.bytes,d.packets,d.records,d.first_seen,d.last_seen,INET6_NTOA(d.src) src,INET6_NTOA(d.dst) dst,d.sport,d.dport,d.protocol FROM (' . $inner . ') d LEFT JOIN (SELECT name,DENSE_RANK() OVER (ORDER BY name) name_id FROM plugin_flowview_nbar_names GROUP BY name) n ON n.name_id=d.name_id ORDER BY d.bytes DESC,d.app_id,d.name_id,d.src,d.dst,d.sport,d.dport,d.protocol,d.router';
}
diff --git a/includes/nbar_seen.php b/includes/nbar_seen.php
index 0d7a53b6..1350d79e 100644
--- a/includes/nbar_seen.php
+++ b/includes/nbar_seen.php
@@ -32,31 +32,34 @@
*/
function fv_nbar_seen_cells(array $row): string {
$html = '';
+
foreach (['first_seen', 'last_seen'] as $field) {
$stamp = (int) ($row[$field] ?? 0);
- $text = $stamp > 0 ? date('Y-m-d H:i', $stamp) : __('Unavailable', 'flowview');
- $hint = __('FlowView observation within the selected period; minute precision. Time zone: %s', date_default_timezone_get(), 'flowview');
+ $text = $stamp > 0 ? date('Y-m-d H:i', $stamp) : __('Unavailable', 'flowview');
+ $hint = __('FlowView observation within the selected period; minute precision. Time zone: %s', date_default_timezone_get(), 'flowview');
$html .= '
' . fv_nbar_h($text) . '
';
}
+
return $html;
}
/**
* Merge observation dates while preserving missing-date information.
*
* @param array $total Accumulated dates and missing-date marker.
- * @param array $row Exported tuple or observation fields.
+ * @param array $row Exported tuple or observation fields.
*
* @return void
*/
function fv_nbar_seen_merge(array &$total, array $row): void {
if (!isset($row['first_seen'],$row['last_seen'])) {
$total['_seen_missing'] = true;
+
return;
}
- $first = (int) $row['first_seen'];
- $last = (int) $row['last_seen'];
+ $first = (int) $row['first_seen'];
+ $last = (int) $row['last_seen'];
$total['first_seen'] = $total['first_seen'] === null ? $first : min($total['first_seen'], $first);
- $total['last_seen'] = $total['last_seen'] === null ? $last : max($total['last_seen'], $last);
+ $total['last_seen'] = $total['last_seen'] === null ? $last : max($total['last_seen'], $last);
}
/**
* Install observation columns and resumable backfill checkpoints.
@@ -67,6 +70,7 @@ function fv_nbar_seen_schema(): bool {
foreach (['', 'daily_'] as $prefix) {
foreach (['apps', 'pairs', 'connections'] as $kind) {
$table = 'plugin_flowview_nbar_' . $prefix . $kind;
+
foreach (['first_seen', 'last_seen'] as $field) {
if (!flowview_db_fetch_row("SHOW COLUMNS FROM $table LIKE '$field'")) {
if (flowview_db_add_column($table, ['name' => $field, 'type' => 'int', 'unsigned' => true, 'NULL' => true]) === false) {
@@ -77,23 +81,28 @@ function fv_nbar_seen_schema(): bool {
}
}
$exists = flowview_db_fetch_cell("SHOW TABLES LIKE 'plugin_flowview_nbar_seen_upgrade'");
+
if (!$exists) {
if (fv_nbar_schema_table('seen_upgrade') === false) {
return false;
}
}
+
// Always retry idempotent seeds, including after an interrupted installation.
foreach (['0' => 'hours', '1' => 'days'] as $kind => $table) {
$field = $kind === 0 ? 'hour' : 'day';
+
if (flowview_db_execute("INSERT IGNORE INTO plugin_flowview_nbar_seen_upgrade (kind,listener_id,period) SELECT $kind,listener_id,$field FROM plugin_flowview_nbar_$table") === false) {
return false;
}
}
+
foreach (['phase' => ['name' => 'phase', 'type' => 'tinyint', 'unsigned' => true, 'NULL' => false, 'default' => 0], 'app_cursor' => ['name' => 'app_cursor', 'type' => 'varchar(32)', 'NULL' => false, 'default' => '']] as $field => $definition) {
if (!flowview_db_fetch_row("SHOW COLUMNS FROM plugin_flowview_nbar_seen_upgrade LIKE '$field'") && flowview_db_add_column('plugin_flowview_nbar_seen_upgrade', $definition) === false) {
return false;
}
}
+
return true;
}
/**
@@ -107,92 +116,113 @@ function fv_nbar_seen_backfill(int $budget = 10): int {
if ((int) flowview_db_fetch_cell("SELECT GET_LOCK(CONCAT('flowview_nbar_summary:',LEFT(SHA2(DATABASE(),256),16)),0)") !== 1) {
return 0;
}
- $daily = false;
- $done = 0;
- $began = microtime(true);
- $old = null;
+ $daily = false;
+ $done = 0;
+ $began = microtime(true);
+ $old = null;
$isolation = flowview_db_fetch_cell('SELECT @@session.transaction_isolation', '', false);
+
if (!in_array($isolation, ['READ-UNCOMMITTED', 'READ-COMMITTED', 'REPEATABLE-READ', 'SERIALIZABLE'], true)) {
$isolation = flowview_db_fetch_cell('SELECT @@session.tx_isolation', '', false);
}
+
try {
$daily = (int) flowview_db_fetch_cell("SELECT GET_LOCK(CONCAT('flowview_nbar_daily:',LEFT(SHA2(DATABASE(),256),16)),0)") === 1;
+
if (!$daily) {
return 0;
}
+
if (!in_array($isolation, ['READ-UNCOMMITTED', 'READ-COMMITTED', 'REPEATABLE-READ', 'SERIALIZABLE'], true)) {
return 0;
}
$old = fv_nbar_worker_timeout(20);
+
if ($old === null || !flowview_db_execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')) {
return 0;
}
$jobs = flowview_db_fetch_assoc('SELECT kind,listener_id,period,phase,app_cursor FROM plugin_flowview_nbar_seen_upgrade WHERE done=0 ORDER BY kind,period DESC,listener_id LIMIT 100');
+
foreach ($jobs as $job) {
if (microtime(true) - $began >= $budget) {
break;
}
$listener = (int) $job['listener_id'];
- $period = (int) $job['period'];
- $isDay = (int) $job['kind'] === 1;
- $end = $period + 3600;
- $prefix = '';
+ $period = (int) $job['period'];
+ $isDay = (int) $job['kind'] === 1;
+ $end = $period + 3600;
+ $prefix = '';
+
if ($isDay) {
$prefix = 'daily_';
- $end = (int) flowview_db_fetch_cell_prepared('SELECT covered_to FROM plugin_flowview_nbar_days WHERE listener_id=? AND day=?', [$listener, $period]);
+ $end = (int) flowview_db_fetch_cell_prepared('SELECT covered_to FROM plugin_flowview_nbar_days WHERE listener_id=? AND day=?', [$listener, $period]);
}
- $ok = true;
+ $ok = true;
$phase = 0;
+
foreach (['apps' => 'app_id,LENGTH(src) family', 'pairs' => 'app_id,src,dst', 'connections' => 'app_id,src,dst,sport,dport,protocol'] as $kind => $fields) {
if ($isDay && $phase < (int) $job['phase']) {
$phase++;
+
continue;
}
- $group = ($kind === 'apps' ? 'app_id,LENGTH(src)' : $fields) . ',router';
- $keys = array_merge($kind === 'apps' ? ['app_id', 'family'] : explode(',', $fields), ['router']);
- $column = flowview_db_fetch_row("SHOW FULL COLUMNS FROM plugin_flowview_nbar_$prefix$kind LIKE 'app_id'");
+ $group = ($kind === 'apps' ? 'app_id,LENGTH(src)' : $fields) . ',router';
+ $keys = array_merge($kind === 'apps' ? ['app_id', 'family'] : explode(',', $fields), ['router']);
+ $column = flowview_db_fetch_row("SHOW FULL COLUMNS FROM plugin_flowview_nbar_$prefix$kind LIKE 'app_id'");
$collation = $column['Collation'];
+
if (!preg_match('/^[a-zA-Z0-9_]+$/D', $collation)) {
throw new RuntimeException('Invalid application collation');
}
- $join = implode(' AND ', array_map(static function (string $k) use ($collation): string {return $k === 'app_id' ? "s.$k=r.$k COLLATE $collation" : "s.$k=r.$k";}, $keys));
- $source = $isDay ? fv_nbar_hourly_source($kind, [$listener], $period, $end) : '(SELECT *,minute first_seen,minute last_seen FROM plugin_flowview_nbar_data)';
- $dates = 'IF(COUNT(first_seen)=COUNT(*),MIN(first_seen),NULL) first_seen,IF(COUNT(last_seen)=COUNT(*),MAX(last_seen),NULL) last_seen';
- $sql = "UPDATE plugin_flowview_nbar_$prefix$kind s JOIN (SELECT $fields,router,SUM(bytes) bytes,SUM(packets) packets,SUM(flows) flows,$dates FROM $source d WHERE listener_id=$listener AND minute>=$period AND minute<$end GROUP BY $group) r ON $join SET s.first_seen=r.first_seen,s.last_seen=r.last_seen WHERE s.listener_id=$listener AND s.hour=$period AND s.bytes=r.bytes AND s.packets=r.packets AND s.flows=r.flows AND (s.first_seen IS NULL OR s.last_seen IS NULL)";
+ $join = implode(' AND ', array_map(static function (string $k) use ($collation): string {return $k === 'app_id' ? "s.$k=r.$k COLLATE $collation" : "s.$k=r.$k"; }, $keys));
+ $source = $isDay ? fv_nbar_hourly_source($kind, [$listener], $period, $end) : '(SELECT *,minute first_seen,minute last_seen FROM plugin_flowview_nbar_data)';
+ $dates = 'IF(COUNT(first_seen)=COUNT(*),MIN(first_seen),NULL) first_seen,IF(COUNT(last_seen)=COUNT(*),MAX(last_seen),NULL) last_seen';
+ $sql = "UPDATE plugin_flowview_nbar_$prefix$kind s JOIN (SELECT $fields,router,SUM(bytes) bytes,SUM(packets) packets,SUM(flows) flows,$dates FROM $source d WHERE listener_id=$listener AND minute>=$period AND minute<$end GROUP BY $group) r ON $join SET s.first_seen=r.first_seen,s.last_seen=r.last_seen WHERE s.listener_id=$listener AND s.hour=$period AND s.bytes=r.bytes AND s.packets=r.packets AND s.flows=r.flows AND (s.first_seen IS NULL OR s.last_seen IS NULL)";
$applications = [null];
+
if ($isDay && $kind === 'connections') {
$applications = array_column(flowview_db_fetch_assoc_prepared('SELECT DISTINCT app_id FROM plugin_flowview_nbar_daily_apps WHERE listener_id=? AND hour=? AND app_id>? ORDER BY app_id', [$listener, $period, ($job['app_cursor'] ?? '')]), 'app_id');
}
+
foreach ($applications as $application) {
if (microtime(true) - $began >= $budget) {
$ok = false;
+
break;
}
$statement = $sql;
+
if ($application !== null) {
- $value = 'CONVERT(0x' . bin2hex($application) . ' USING utf8mb4) COLLATE ' . $collation;
+ $value = 'CONVERT(0x' . bin2hex($application) . ' USING utf8mb4) COLLATE ' . $collation;
$statement = str_replace(' GROUP BY ', " AND app_id=$value GROUP BY ", $statement);
$statement .= " AND s.app_id=$value";
}
+
if (flowview_db_execute($statement) === false) {
$ok = false;
+
break;
}
+
if ($application !== null) {
flowview_db_execute_prepared('UPDATE plugin_flowview_nbar_seen_upgrade SET app_cursor=? WHERE kind=? AND listener_id=? AND period=?', [$application, $job['kind'], $listener, $period]);
}
}
+
if (!$ok) {
break;
}
$phase++;
+
if ($isDay) {
flowview_db_execute_prepared('UPDATE plugin_flowview_nbar_seen_upgrade SET phase=? WHERE kind=? AND listener_id=? AND period=?', [$phase, $job['kind'], $listener, $period]);
}
}
+
if (!$ok) {
break;
}
+
if (flowview_db_execute_prepared('UPDATE plugin_flowview_nbar_seen_upgrade SET done=1 WHERE kind=? AND listener_id=? AND period=?', [$job['kind'], $listener, $period]) === false) {
break;
}
@@ -204,13 +234,16 @@ function fv_nbar_seen_backfill(int $budget = 10): int {
if ($old !== null) {
flowview_db_execute('SET SESSION max_statement_time=' . $old);
}
+
if (in_array($isolation, ['READ-UNCOMMITTED', 'READ-COMMITTED', 'REPEATABLE-READ', 'SERIALIZABLE'], true)) {
flowview_db_execute('SET SESSION TRANSACTION ISOLATION LEVEL ' . str_replace('-', ' ', $isolation));
}
+
if ($daily) {
flowview_db_fetch_cell("SELECT RELEASE_LOCK(CONCAT('flowview_nbar_daily:',LEFT(SHA2(DATABASE(),256),16)))");
}
flowview_db_fetch_cell("SELECT RELEASE_LOCK(CONCAT('flowview_nbar_summary:',LEFT(SHA2(DATABASE(),256),16)))");
}
+
return $done;
}
diff --git a/includes/nbar_summary.php b/includes/nbar_summary.php
index 0bf6cc00..011bd862 100644
--- a/includes/nbar_summary.php
+++ b/includes/nbar_summary.php
@@ -37,19 +37,21 @@ function fv_nbar_summary_schema(): bool {
}
}
$column = flowview_db_fetch_row("SHOW COLUMNS FROM plugin_flowview_nbar_hours LIKE 'connection_built'");
+
if (!$column && flowview_db_add_column('plugin_flowview_nbar_hours', ['name' => 'connection_built', 'type' => 'bigint', 'NULL' => true, 'default' => -1]) === false) {
return false;
}
+
return fv_nbar_daily_schema() && fv_nbar_seen_schema();
}
/**
* Write raw records and invalidate affected hours in one transaction.
*
- * @param string $sql Trusted SQL statement.
- * @param int $listener Capture identifier.
- * @param array $hours Affected UTC hour boundaries.
- * @param array $args Prepared statement values.
+ * @param string $sql Trusted SQL statement.
+ * @param int $listener Capture identifier.
+ * @param array $hours Affected UTC hour boundaries.
+ * @param array $args Prepared statement values.
*
* @return bool
*/
@@ -58,17 +60,22 @@ function fv_nbar_summary_store(string $sql, int $listener, array $hours, array $
return false;
}
$ok = flowview_db_execute_prepared($sql, $args) !== false;
+
if ($ok) {
$values = [];
+
foreach (array_keys($hours) as $hour) {
$values[] = '(' . (int) $listener . ',' . (int) $hour . ',1,-1)';
}
$ok = flowview_db_execute('INSERT INTO plugin_flowview_nbar_hours (listener_id,hour,revision,built) VALUES ' . implode(',', $values) . ' ON DUPLICATE KEY UPDATE revision=revision+1') !== false;
}
+
if (!$ok) {
flowview_db_execute('ROLLBACK');
+
return false;
}
+
return flowview_db_execute('COMMIT') !== false;
}
@@ -81,34 +88,42 @@ function fv_nbar_summary_store(string $sql, int $listener, array $hours, array $
*/
function fv_nbar_summary_refresh(int $budget = 20, bool $supervised = false): int {
$lock = flowview_db_fetch_row("SELECT GET_LOCK(CONCAT('flowview_nbar_summary:',LEFT(SHA2(DATABASE(),256),16)),0) acquired");
+
if ((int) ($lock['acquired'] ?? 0) !== 1) {
return 0;
}
- $done = 0;
+ $done = 0;
$started = microtime(true);
+
try {
- $cutoff = time() - fv_nbar_retention_days() * 86400;
- $closed = (int) (floor((time() - 300) / 3600) * 3600);
+ $cutoff = time() - fv_nbar_retention_days() * 86400;
+ $closed = (int) (floor((time() - 300) / 3600) * 3600);
$listeners = flowview_db_fetch_assoc('SELECT id FROM plugin_flowview_devices');
+
foreach ($listeners as $listener) {
- $id = (int) $listener['id'];
+ $id = (int) $listener['id'];
$first = flowview_db_fetch_row_prepared('SELECT minute FROM plugin_flowview_nbar_data WHERE listener_id=? ORDER BY minute LIMIT 1', [$id]);
+
if (!isset($first['minute'])) {
continue;
}
$values = [];
+
for ($h = (int) (floor(max($cutoff, (int) $first['minute']) / 3600) * 3600); $h < $closed; $h += 3600) {
$values[] = "($id,$h,0,-1)";
}
+
if ($values && flowview_db_execute('INSERT IGNORE INTO plugin_flowview_nbar_hours (listener_id,hour,revision,built) VALUES ' . implode(',', $values)) === false) {
return $done;
}
}
- $pending = flowview_db_fetch_assoc("SELECT listener_id,hour,revision FROM plugin_flowview_nbar_hours WHERE hour>=" . (int) (ceil($cutoff / 3600) * 3600) . " AND hour<$closed AND (built<0 OR built<>revision OR connection_built<0 OR connection_built<>revision) ORDER BY hour DESC,listener_id LIMIT 100");
+ $pending = flowview_db_fetch_assoc('SELECT listener_id,hour,revision FROM plugin_flowview_nbar_hours WHERE hour>=' . (int) (ceil($cutoff / 3600) * 3600) . " AND hour<$closed AND (built<0 OR built<>revision OR connection_built<0 OR connection_built<>revision) ORDER BY hour DESC,listener_id LIMIT 100");
+
foreach ($pending as $item) {
if (microtime(true) - $started >= max(1, (int) $budget)) {
break;
}
+
if (fv_nbar_summary_build((int) $item['listener_id'], (int) $item['hour'], $supervised)) {
$done++;
}
@@ -116,6 +131,7 @@ function fv_nbar_summary_refresh(int $budget = 20, bool $supervised = false): in
} finally {
flowview_db_fetch_row("SELECT RELEASE_LOCK(CONCAT('flowview_nbar_summary:',LEFT(SHA2(DATABASE(),256),16))) released");
}
+
return $done + fv_nbar_daily_refresh() + fv_nbar_seen_backfill(5);
}
@@ -123,54 +139,64 @@ function fv_nbar_summary_refresh(int $budget = 20, bool $supervised = false): in
* Publish an hourly summary only if its raw revision remains unchanged.
*
* @param int $listener Capture identifier.
- * @param int $hour UTC hour boundary in Unix seconds.
+ * @param int $hour UTC hour boundary in Unix seconds.
*
* @return bool
*/
function fv_nbar_summary_build(int $listener, int $hour, bool $supervised = false): bool {
// A MySQL build without DML timeouts is allowed only in the supervised child.
$old_timeout = fv_nbar_worker_timeout(20);
+
if ($old_timeout === null && !$supervised) {
return false;
}
+
try {
// READ COMMITTED avoids holding raw-record locks during INSERT ... SELECT.
if (flowview_db_execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED') === false || flowview_db_execute('START TRANSACTION') === false) {
return false;
}
$state = flowview_db_fetch_row_prepared('SELECT revision FROM plugin_flowview_nbar_hours WHERE listener_id=? AND hour=?', [$listener, $hour]);
+
if (!isset($state['revision'])) {
throw new RuntimeException('Missing summary state');
}
$where = ' FROM plugin_flowview_nbar_data WHERE listener_id=? AND minute>=? AND minute';
- $args = [$listener, $hour, $hour + 3600];
+ $args = [$listener, $hour, $hour + 3600];
$specs = [
- 'apps' => 'app_id,LENGTH(src)',
- 'pairs' => 'app_id,src,dst',
+ 'apps' => 'app_id,LENGTH(src)',
+ 'pairs' => 'app_id,src,dst',
'connections' => 'app_id,src,dst,sport,dport,protocol'
];
+
foreach ($specs as $table => $fields) {
if (flowview_db_execute_prepared("DELETE FROM plugin_flowview_nbar_$table WHERE listener_id=? AND hour=?", [$listener, $hour]) === false) {
throw new RuntimeException('Summary delete failed');
}
$sql = "INSERT INTO plugin_flowview_nbar_$table SELECT $listener,$hour,$fields,SUM(bytes),SUM(packets),SUM(flows),MIN(minute),MAX(minute),router$where GROUP BY $fields,router";
+
if (flowview_db_execute_prepared($sql, $args) === false) {
throw new RuntimeException('Summary aggregate failed');
}
}
$latest = flowview_db_fetch_row_prepared('SELECT revision FROM plugin_flowview_nbar_hours WHERE listener_id=? AND hour=?', [$listener, $hour]);
+
if (!isset($latest['revision']) || (string) $latest['revision'] !== (string) $state['revision']) {
throw new RuntimeException('Hour changed during summary build');
}
+
if (flowview_db_execute_prepared('UPDATE plugin_flowview_nbar_hours SET built=?,connection_built=? WHERE listener_id=? AND hour=?', [$state['revision'], $state['revision'], $listener, $hour]) === false) {
throw new RuntimeException('Summary publish failed');
}
+
if (flowview_db_execute('COMMIT') === false) {
throw new RuntimeException('Summary commit failed');
}
+
return true;
} catch (Throwable $e) {
flowview_db_execute('ROLLBACK');
+
return false;
} finally {
if ($old_timeout !== null) {
@@ -182,11 +208,11 @@ function fv_nbar_summary_build(int $listener, int $hour, bool $supervised = fals
/**
* Choose mutually exclusive ready-summary or raw branches for each block.
*
- * @param string $kind Allowlisted summary type.
- * @param array $listeners Selected capture identifiers.
- * @param int $start Inclusive range start in Unix seconds.
- * @param int $end Exclusive range end in Unix seconds.
- * @param int|null $now Current Unix time, or null to read the clock.
+ * @param string $kind Allowlisted summary type.
+ * @param array $listeners Selected capture identifiers.
+ * @param int $start Inclusive range start in Unix seconds.
+ * @param int $end Exclusive range end in Unix seconds.
+ * @param int|null $now Current Unix time, or null to read the clock.
*
* @return string
*/
@@ -194,33 +220,40 @@ function fv_nbar_hourly_source(string $kind, array $listeners, int $start, int $
if (!in_array($kind, ['apps', 'pairs', 'connections'], true)) {
throw new InvalidArgumentException('Unknown summary kind');
}
- $listeners = array_values(array_unique(array_filter(array_map('intval', $listeners), static function (int $id): bool {return $id > 0;})));
+ $listeners = array_values(array_unique(array_filter(array_map('intval', $listeners), static function (int $id): bool {return $id > 0; })));
+
if (!$listeners) {
return '(SELECT listener_id,minute,app_id,src,dst,sport,dport,protocol,bytes,packets,flows,minute first_seen,minute last_seen,router FROM plugin_flowview_nbar_data)';
}
- $start = (int) $start;
- $end = (int) $end;
+ $start = (int) $start;
+ $end = (int) $end;
$full_start = (int) (ceil($start / 3600) * 3600);
- $full_end = min((int) (floor($end / 3600) * 3600), (int) (floor((($now ?? time()) - 300) / 3600) * 3600));
+ $full_end = min((int) (floor($end / 3600) * 3600), (int) (floor((($now ?? time()) - 300) / 3600) * 3600));
+
if ($full_end <= $full_start) {
return '(SELECT listener_id,minute,app_id,src,dst,sport,dport,protocol,bytes,packets,flows,minute first_seen,minute last_seen,router FROM plugin_flowview_nbar_data)';
}
$built_column = $kind === 'connections' ? 'connection_built' : 'built';
- $parts = [];
+ $parts = [];
+
foreach ($listeners as $listener) {
$raw = 'listener_id,minute,app_id,src,dst,sport,dport,protocol,bytes,packets,flows,minute first_seen,minute last_seen,router';
+
if ($start < $full_start) {
$parts[] = "SELECT $raw FROM plugin_flowview_nbar_data WHERE listener_id=$listener AND minute>=$start AND minute<$full_start";
}
+
if ($full_end < $end) {
$parts[] = "SELECT $raw FROM plugin_flowview_nbar_data WHERE listener_id=$listener AND minute>=$full_end AND minute<$end";
}
+
for ($h = $full_start; $h < $full_end; $h = $to) {
- $to = min($h + 21600, $full_end);
+ $to = min($h + 21600, $full_end);
$count = (int) (($to - $h) / 3600);
$ready = "(SELECT COUNT(*) FROM plugin_flowview_nbar_hours WHERE listener_id=$listener AND hour>=$h AND hour<$to AND $built_column>=0 AND $built_column=revision)=$count";
+
if ($kind === 'apps') {
- $fields = "listener_id,hour minute,app_id,REPEAT(CHAR(0),family) src,REPEAT(CHAR(0),family) dst,0 sport,0 dport,0 protocol,bytes,packets,flows,first_seen,last_seen,router";
+ $fields = 'listener_id,hour minute,app_id,REPEAT(CHAR(0),family) src,REPEAT(CHAR(0),family) dst,0 sport,0 dport,0 protocol,bytes,packets,flows,first_seen,last_seen,router';
} elseif ($kind === 'pairs') {
$fields = 'listener_id,hour minute,app_id,src,dst,0 sport,0 dport,0 protocol,bytes,packets,flows,first_seen,last_seen,router';
} else {
@@ -230,5 +263,6 @@ function fv_nbar_hourly_source(string $kind, array $listeners, int $start, int $
$parts[] = "SELECT $raw FROM plugin_flowview_nbar_data WHERE listener_id=$listener AND minute>=$h AND minute<$to AND NOT ($ready)";
}
}
+
return '(' . implode(' UNION ALL ', $parts) . ')';
}
diff --git a/includes/nbar_timespan.php b/includes/nbar_timespan.php
index fd780ccb..1d41b552 100644
--- a/includes/nbar_timespan.php
+++ b/includes/nbar_timespan.php
@@ -26,21 +26,23 @@
/**
* Limit preset choices to the configured retention window.
*
- * @param array $presets Cacti timespan labels by identifier.
- * @param int $now Current Unix time.
- * @param int $days Configured retention in days.
+ * @param array $presets Cacti timespan labels by identifier.
+ * @param int $now Current Unix time.
+ * @param int $days Configured retention in days.
* @param int|string $weekday Cacti first-weekday setting.
*
* @return array
*/
function fv_nbar_timespan_options(array $presets, int $now, int $days, int|string $weekday): array {
$options = [];
- $cutoff = $now - $days * 86400;
+ $cutoff = $now - $days * 86400;
+
foreach ($presets as $id => $label) {
$span = [];
get_timespan($span, $now, $id, $weekday);
$from = strtotime($span['current_value_date1']);
- $to = strtotime($span['current_value_date2']);
+ $to = strtotime($span['current_value_date2']);
+
if ($from >= $cutoff && $to > $from && $to <= $now) {
// The exact retention option below replaces duplicate rolling periods.
if ($from === $cutoff && $to === $now) {
@@ -50,25 +52,28 @@ function fv_nbar_timespan_options(array $presets, int $now, int $days, int|strin
}
}
$options[1000] = $days === 1 ? __('Last Day', 'flowview') : __('Last %s Days', $days, 'flowview');
+
return $options;
}
/**
* Clamp a requested range to retained history and the current time.
*
* @param int $start Inclusive range start in Unix seconds.
- * @param int $end Exclusive range end in Unix seconds.
- * @param int $now Current Unix time.
- * @param int $days Configured retention in days.
+ * @param int $end Exclusive range end in Unix seconds.
+ * @param int $now Current Unix time.
+ * @param int $days Configured retention in days.
*
* @return array
*/
function fv_nbar_bound_dates(int $start, int $end, int $now, int $days): array {
$cutoff = $now - $days * 86400;
- $from = max($cutoff, $start);
- $to = min($now, $end);
+ $from = max($cutoff, $start);
+ $to = min($now, $end);
+
if ($to <= $from) {
return [$now - 1800, $now, true];
}
+
return [$from, $to, $from !== $start || $to !== $end];
}
@@ -78,12 +83,14 @@ function fv_nbar_timespan_request(): bool {
return false;
}
header('Content-Type: application/json');
+
if ((int) get_filter_request_var('predefined_timespan') === 1000) {
- $end = (int) (floor(time() / 60) * 60);
+ $end = (int) (floor(time() / 60) * 60);
$start = $end - fv_nbar_retention_days() * 86400;
print json_encode(['current_value_date1' => date('Y-m-d H:i', $start), 'current_value_date2' => date('Y-m-d H:i', $end), 'begin_now' => $start, 'end_now' => $end]);
} else {
flowview_gettimespan();
}
+
return true;
}
diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot
index 855f1b50..0ee096ca 100644
--- a/locales/po/cacti.pot
+++ b/locales/po/cacti.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti 5.1\n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
-"POT-Creation-Date: 2026-10-17 22:46+1030\n"
+"POT-Creation-Date: 2026-10-19 12:10-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers \n"
"Language-Team: Cacti Developers \n"
@@ -518,7 +518,7 @@ msgstr[0] ""
msgstr[1] ""
#: flowview_databases.php flowview_devices.php flowview_filters.php
-#: flowview_schedules.php includes/functions.php nbar_preferences.php
+#: flowview_schedules.php includes/functions.php includes/nbar_preferences.php
msgid "Cancel"
msgstr ""
@@ -1926,7 +1926,7 @@ msgstr ""
msgid "Edit the Flow Filter"
msgstr ""
-#: includes/functions.php nbar_preferences.php
+#: includes/functions.php includes/nbar_preferences.php
msgid "Save"
msgstr ""
@@ -2417,202 +2417,237 @@ msgstr ""
msgid "InnoDB (Slow, High Concurrency)"
msgstr ""
-#: nbar_chart.php
+#: includes/nbar_chart.php
+msgid "Unable to load application chart. Please try again."
+msgstr ""
+
+#: includes/nbar_chart.php
msgid "Other"
msgstr ""
-#: nbar_chart.php
+#: includes/nbar_chart.php
msgid "Top 5 applications"
msgstr ""
-#: nbar_chart.php
+#: includes/nbar_chart.php
msgid "Top 5 applications by data"
msgstr ""
-#: nbar_chart.php
+#: includes/nbar_chart.php
msgid "Application data share for the selected period"
msgstr ""
-#: nbar_chart.php
+#: includes/nbar_chart.php
msgid "No application data in this period"
msgstr ""
-#: nbar_conversations.php
-msgid "Configure your site networks in Application settings to label internal and external devices. Top applications and Connections work directly with exported IPv4 and IPv6 addresses."
+#: includes/nbar_dns.php
+msgid "No reverse DNS"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "← Back to top applications"
+#: includes/nbar_dns.php
+msgid "Not resolved"
msgstr ""
-#: nbar_conversations.php
-msgid "Internal Host ↔ External Host. Click a device to see its individual connections. Bandwidth is averaged over the selected timespan."
+#: includes/nbar_feed_status.php
+msgid "STATUS:"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "Application"
+#: includes/nbar_feed_status.php
+msgid "Unable to load NBAR status. Please try again."
msgstr ""
-#: nbar_conversations.php
-msgid "Download (Avg Mbps)"
+#: includes/nbar_feed_status.php
+msgid "No recent NBAR"
msgstr ""
-#: nbar_conversations.php
-msgid "Download (GB)"
+#: includes/nbar_feed_status.php
+msgid "Receiving"
msgstr ""
-#: nbar_conversations.php
-msgid "External Host"
+#: includes/nbar_feed_status.php
+#, php-format
+msgid "Last NBAR record: %s"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "First seen"
+#: includes/nbar_feed_status.php
+msgid "No retained NBAR records"
msgstr ""
-#: nbar_conversations.php
-msgid "Internal Host"
+#: includes/nbar_feed_status.php
+msgid "No captures selected"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "Last seen"
+#: includes/nbar_preferences.php
+msgid "Settings were not saved. Enter valid IPv4/IPv6 networks, or leave the list blank."
msgstr ""
-#: nbar_conversations.php
-msgid "Upload (Avg Mbps)"
+#: includes/nbar_preferences.php
+msgid "Settings were not saved. Choose two or three captures to combine, or clear all selections to save Internal Networks only."
msgstr ""
-#: nbar_conversations.php
-msgid "Upload (GB)"
+#: includes/nbar_preferences.php
+msgid "Bandwidth units"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-#, php-format
-msgid "Awaiting name (%s)"
+#: includes/nbar_preferences.php
+msgid "DNS names"
msgstr ""
-#: nbar_conversations.php
-msgid "No conversations for this period. Allow 1–2 minutes for exports, or choose a longer timespan."
+#: includes/nbar_preferences.php
+msgid "Data units"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates."
+#: includes/nbar_preferences.php
+msgid "Hide"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "Report information"
+#: includes/nbar_preferences.php nbar_report.php
+msgid "Show"
msgstr ""
-#: nbar_conversations.php nbar_report.php
-msgid "Drag to resize column"
+#: includes/nbar_preferences.php
+msgid "Auto"
msgstr ""
-#: nbar_dns.php
-msgid "No reverse DNS"
+#: includes/nbar_preferences.php nbar_setup_guide.php
+msgid "Close"
msgstr ""
-#: nbar_dns.php
-msgid "Not resolved"
+#: includes/nbar_preferences.php
+msgid "Combine Options"
msgstr ""
-#: nbar_feed_status.php
-msgid "STATUS:"
+#: includes/nbar_preferences.php nbar_report.php
+msgid "Options"
msgstr ""
-#: nbar_feed_status.php
-msgid "No recent NBAR"
+#: includes/nbar_preferences.php
+msgid "Choose two or three captures to combine, normally one LAN capture per router. Leave all captures unticked to save Internal Networks only. Overlapping LAN and WAN captures are added together and can count traffic twice."
msgstr ""
-#: nbar_feed_status.php
-msgid "Receiving"
+#: includes/nbar_preferences.php
+msgid "Internal Networks"
+msgstr ""
+
+#: includes/nbar_preferences.php
+msgid "One internal IPv4 or IPv6 network per line. Used to identify internal hosts and upload/download direction in Conversations. Update delegated IPv6 prefixes when they change, and keep old prefixes for retained history. Exported flows do not automatically identify your site networks."
+msgstr ""
+
+#: includes/nbar_preferences.php
+msgid " — combining off"
msgstr ""
-#: nbar_feed_status.php
+#: includes/nbar_preferences.php
+msgid " — select another capture or untick all to save"
+msgstr ""
+
+#: includes/nbar_preferences.php
+msgid "Average rate across the selected timespan."
+msgstr ""
+
+#: includes/nbar_preferences.php
+msgid "Decimal units: 1 GB = 1,000 MB."
+msgstr ""
+
+#: includes/nbar_preferences.php
+msgid "{count} of 3 selected"
+msgstr ""
+
+#: includes/nbar_seen.php
+msgid "Unavailable"
+msgstr ""
+
+#: includes/nbar_seen.php
#, php-format
-msgid "Last NBAR record: %s"
+msgid "FlowView observation within the selected period; minute precision. Time zone: %s"
msgstr ""
-#: nbar_feed_status.php
-msgid "No retained NBAR records"
+#: includes/nbar_timespan.php
+#, php-format
+msgid "Last %s Days"
msgstr ""
-#: nbar_feed_status.php
-msgid "No captures selected"
+#: includes/nbar_timespan.php
+msgid "Last Day"
msgstr ""
-#: nbar_preferences.php
-msgid "Settings were not saved. Enter valid IPv4/IPv6 networks, or leave the list blank."
+#: nbar_conversations.php
+msgid "Configure your site networks in Application settings to label internal and external devices. Top applications and Connections work directly with exported IPv4 and IPv6 addresses."
msgstr ""
-#: nbar_preferences.php
-msgid "Settings were not saved. Choose two or three captures to combine, or clear all selections to save Internal Networks only."
+#: nbar_conversations.php
+msgid "Unable to load conversations. Please try again."
msgstr ""
-#: nbar_preferences.php
-msgid "Bandwidth units"
+#: nbar_conversations.php nbar_report.php
+msgid "← Back to top applications"
msgstr ""
-#: nbar_preferences.php
-msgid "DNS names"
+#: nbar_conversations.php
+msgid "Internal Host ↔ External Host. Click a device to see its individual connections. Bandwidth is averaged over the selected timespan."
msgstr ""
-#: nbar_preferences.php
-msgid "Data units"
+#: nbar_conversations.php nbar_report.php
+msgid "Application"
msgstr ""
-#: nbar_preferences.php
-msgid "Hide"
+#: nbar_conversations.php
+msgid "Download (Avg Mbps)"
msgstr ""
-#: nbar_preferences.php nbar_report.php
-msgid "Show"
+#: nbar_conversations.php
+msgid "Download (GB)"
msgstr ""
-#: nbar_preferences.php
-msgid "Auto"
+#: nbar_conversations.php
+msgid "External Host"
msgstr ""
-#: nbar_preferences.php nbar_setup_guide.php
-msgid "Close"
+#: nbar_conversations.php nbar_report.php
+msgid "First seen"
msgstr ""
-#: nbar_preferences.php
-msgid "Combine Options"
+#: nbar_conversations.php
+msgid "Internal Host"
msgstr ""
-#: nbar_preferences.php nbar_report.php
-msgid "Options"
+#: nbar_conversations.php nbar_report.php
+msgid "Last seen"
msgstr ""
-#: nbar_preferences.php
-msgid "Choose two or three captures to combine, normally one LAN capture per router. Leave all captures unticked to save Internal Networks only. Overlapping LAN and WAN captures are added together and can count traffic twice."
+#: nbar_conversations.php
+msgid "Upload (Avg Mbps)"
msgstr ""
-#: nbar_preferences.php
-msgid "Internal Networks"
+#: nbar_conversations.php
+msgid "Upload (GB)"
msgstr ""
-#: nbar_preferences.php
-msgid "One internal IPv4 or IPv6 network per line. Used to identify internal hosts and upload/download direction in Conversations. Update delegated IPv6 prefixes when they change, and keep old prefixes for retained history. Exported flows do not automatically identify your site networks."
+#: nbar_conversations.php nbar_report.php
+#, php-format
+msgid "Awaiting name (%s)"
msgstr ""
-#: nbar_preferences.php
-msgid " — combining off"
+#: nbar_conversations.php
+msgid "No conversations for this period. Allow 1–2 minutes for exports, or choose a longer timespan."
msgstr ""
-#: nbar_preferences.php
-msgid " — select another capture or untick all to save"
+#: nbar_conversations.php nbar_report.php
+msgid "First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates."
msgstr ""
-#: nbar_preferences.php
-msgid "Average rate across the selected timespan."
+#: nbar_conversations.php nbar_report.php
+msgid "Report information"
msgstr ""
-#: nbar_preferences.php
-msgid "Decimal units: 1 GB = 1,000 MB."
+#: nbar_conversations.php
+#, php-format
+msgid "Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: %d days."
msgstr ""
-#: nbar_preferences.php
-msgid "{count} of 3 selected"
+#: nbar_conversations.php nbar_report.php
+msgid "Drag to resize column"
msgstr ""
#: nbar_report.php
@@ -2685,6 +2720,10 @@ msgstr ""
msgid "Unable to load connections. Try a shorter timespan or filter by IP."
msgstr ""
+#: nbar_report.php
+msgid "Unable to load applications. Please try again."
+msgstr ""
+
#: nbar_report.php
msgid "Counting total pages…"
msgstr ""
@@ -2762,15 +2801,6 @@ msgstr ""
msgid "Results are ordered by data volume; Connections shows up to 100 per page. Column sorting applies to the displayed page. NBAR history uses the configured retention setting. Daily summaries are reused across report selections; partial days use hourly summaries, and partial or changed hours use raw records. Connections pages are read from shared daily history, with nearby pages briefly cached for navigation. Average Mbps covers the selected period; allow 1–2 minutes for exports. Capture visibility depends on the device exporter and observation point. NAT can hide internal IPv4 addresses on a WAN capture. Select one observation point per traffic path to avoid duplicate totals. Cisco NBAR may classify encrypted traffic as SSL, QUIC or Unknown. History starts when export is enabled."
msgstr ""
-#: nbar_seen.php
-msgid "Unavailable"
-msgstr ""
-
-#: nbar_seen.php
-#, php-format
-msgid "FlowView observation within the selected period; minute precision. Time zone: %s"
-msgstr ""
-
#: nbar_setup_guide.php
msgid "1. FlowView listener"
msgstr ""
@@ -2851,15 +2881,6 @@ msgstr ""
msgid "Reading reports"
msgstr ""
-#: nbar_timespan.php
-#, php-format
-msgid "Last %s Days"
-msgstr ""
-
-#: nbar_timespan.php
-msgid "Last Day"
-msgstr ""
-
#: setup.php
msgid "NetFlow User"
msgstr ""
@@ -3278,24 +3299,3 @@ msgstr ""
#: setup.php
msgid "NetFlow"
msgstr ""
-
-#: nbar_chart.php
-msgid "Unable to load application chart. Please try again."
-msgstr ""
-
-#: nbar_chart.php
-msgid "Unable to load conversations. Please try again."
-msgstr ""
-
-#: nbar_report.php
-msgid "Unable to load applications. Please try again."
-msgstr ""
-
-#: nbar_feed_status.php
-msgid "Unable to load NBAR status. Please try again."
-msgstr ""
-
-#: nbar_conversations.php
-#, php-format
-msgid "Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: %d days."
-msgstr ""
diff --git a/nbar_conversations.php b/nbar_conversations.php
index 397f4b49..16357e53 100644
--- a/nbar_conversations.php
+++ b/nbar_conversations.php
@@ -25,18 +25,22 @@
// Conversation direction uses configured site networks and selected listeners.
if (empty($nbar_preferences['prefixes'])) {
- echo '
' . html_escape(__('Configure your site networks in Application settings to label internal and external devices. Top applications and Connections work directly with exported IPv4 and IPv6 addresses.', 'flowview')) . '
';
+ print '
' . html_escape(__('Configure your site networks in Application settings to label internal and external devices. Top applications and Connections work directly with exported IPv4 and IPv6 addresses.', 'flowview')) . '
';
+
return;
}
$where = $listener_scope . ' AND d.minute>=? AND d.minute';
-$args = $lid === 0 ? [$start, $end] : [$lid, $start, $end];
+$args = $lid === 0 ? [$start, $end] : [$lid, $start, $end];
+
if ($family !== 'all') {
$where .= ' AND LENGTH(d.src)=' . ($family === '6' ? 16 : 4);
}
+
if ($aid !== '') {
$where .= ' AND d.app_id=?';
$args[] = $aid;
}
+
if ($host !== '') {
$where .= ' AND (d.src=INET6_ATON(?) OR d.dst=INET6_ATON(?))';
array_push($args, $host, $host);
@@ -45,49 +49,57 @@
$dl = fv_nbar_local_sql('d.dst');
$where .= ' AND ' . fv_nbar_unicast_sql('d.src') . ' AND ' . fv_nbar_unicast_sql('d.dst');
$where .= " AND (($sl AND NOT $dl) OR ($dl AND NOT $sl))";
-$fields = "d.app_id,n.name,INET6_NTOA(IF($sl,d.src,d.dst)) internal,INET6_NTOA(IF($sl,d.dst,d.src)) external,SUM(IF($sl,d.bytes,0)) upload,SUM(IF($dl,d.bytes,0)) download,SUM(d.bytes) total,IF(COUNT(d.first_seen)=COUNT(*),MIN(d.first_seen),NULL) first_seen,IF(COUNT(d.last_seen)=COUNT(*),MAX(d.last_seen),NULL) last_seen";
+$fields = "d.app_id,n.name,INET6_NTOA(IF($sl,d.src,d.dst)) internal,INET6_NTOA(IF($sl,d.dst,d.src)) external,SUM(IF($sl,d.bytes,0)) upload,SUM(IF($dl,d.bytes,0)) download,SUM(d.bytes) total,IF(COUNT(d.first_seen)=COUNT(*),MIN(d.first_seen),NULL) first_seen,IF(COUNT(d.last_seen)=COUNT(*),MAX(d.last_seen),NULL) last_seen";
$nbar_source = fv_nbar_summary_source('pairs', $lid === 0 ? $combined_lids : [$lid], $start, $end);
-$sql = "SELECT $fields FROM $nbar_source d JOIN plugin_flowview_devices l ON l.id=d.listener_id LEFT JOIN plugin_flowview_nbar_names n ON n.router=COALESCE(NULLIF(d.router,SPACE(0)),l.allowfrom) AND n.app_id=d.app_id WHERE $where GROUP BY d.app_id,n.name,internal,external ORDER BY total DESC LIMIT 100";
-$rows = fv_nbar_query_cached($sql, $args);
+$sql = "SELECT $fields FROM $nbar_source d JOIN plugin_flowview_devices l ON l.id=d.listener_id LEFT JOIN plugin_flowview_nbar_names n ON n.router=COALESCE(NULLIF(d.router,SPACE(0)),l.allowfrom) AND n.app_id=d.app_id WHERE $where GROUP BY d.app_id,n.name,internal,external ORDER BY total DESC LIMIT 100";
+$rows = fv_nbar_query_cached($sql, $args);
+
if (!is_array($rows)) {
- echo '
' . html_escape(__('Internal Host ↔ External Host. Click a device to see its individual connections. Bandwidth is averaged over the selected timespan.', 'flowview')) . '
';
-echo '
';
+print '
' . html_escape(__('Internal Host ↔ External Host. Click a device to see its individual connections. Bandwidth is averaged over the selected timespan.', 'flowview')) . '
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
' . html_escape(__('Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: %d days.', fv_nbar_retention_days(), 'flowview')) . '
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
' . html_escape(__('Top 100 conversations by combined data volume. Each row groups an Internal Host, External Host and application across both directions. Uses only the selected LAN captures to avoid counting WAN copies. Local and multicast traffic is excluded. DNS names identify servers, not the exact website requested. History retention: %d days.', fv_nbar_retention_days(), 'flowview')) . '
' . html_escape(__('First seen and Last seen show the earliest and latest FlowView observations within the selected period, at minute precision. They do not identify continuous activity or exact flow start/end times. Unavailable means the retained summary has no complete observation dates.', 'flowview')) . '
' . html_escape(__('Results are ordered by data volume; Connections shows up to 100 per page. Column sorting applies to the displayed page. NBAR history uses the configured retention setting. Daily summaries are reused across report selections; partial days use hourly summaries, and partial or changed hours use raw records. Connections pages are read from shared daily history, with nearby pages briefly cached for navigation. Average Mbps covers the selected period; allow 1–2 minutes for exports. Capture visibility depends on the device exporter and observation point. NAT can hide internal IPv4 addresses on a WAN capture. Select one observation point per traffic path to avoid duplicate totals. Cisco NBAR may classify encrypted traffic as SSL, QUIC or Unknown. History starts when export is enabled.', 'flowview')) . '
';
}
/**
* Close the fixture report-box markup.
@@ -58,7 +61,7 @@ function html_start_box(mixed ...$args): void {
* @return void
*/
function html_end_box(mixed ...$args): void {
- echo '
';
+ print '';
}
/**
* Record completion without loading Cacti's real page footer.
@@ -68,13 +71,15 @@ function html_end_box(mixed ...$args): void {
function bottom_footer(): void {
$GLOBALS['__test_footer_rendered'] = true;
}
-$GLOBALS['__test_config']['flowview_nbar_schema_version'] = '2';
+$GLOBALS['__test_config']['flowview_nbar_schema_version'] = '2';
$GLOBALS['__test_user_settings']['flowview_nbar_preferences'] = json_encode(['prefixes' => ['192.0.2.0/24'], 'listeners' => [], 'dns' => '0']);
-$graph_timespans = [GT_LAST_HALF_HOUR => 'Last Half Hour', GT_LAST_HOUR => 'Last Hour', GT_LAST_DAY => 'Last Day', GT_LAST_YEAR => 'Last Year'];
-$_REQUEST = ['action' => 'view', 'report' => 'nbar', 'mode' => 'apps', 'listener' => '1'];
-$_SERVER['REQUEST_METHOD'] = 'GET';
+$graph_timespans = [GT_LAST_HALF_HOUR => 'Last Half Hour', GT_LAST_HOUR => 'Last Hour', GT_LAST_DAY => 'Last Day', GT_LAST_YEAR => 'Last Year'];
+$_REQUEST = ['action' => 'view', 'report' => 'nbar', 'mode' => 'apps', 'listener' => '1'];
+$_SERVER['REQUEST_METHOD'] = 'GET';
+
if (str_starts_with($scenario, 'nav-redirect')) {
$_REQUEST = in_array($scenario, ['nav-redirect-header', 'nav-redirect-fragment-header'], true) ? ['headercontent' => 'true'] : [];
+
if (str_contains($scenario, 'fragment')) {
$_REQUEST += ['action' => 'view', 'header' => 'false'];
}
@@ -120,15 +125,17 @@ function bottom_footer(): void {
plugin_test_queue_db_result('db_execute', false);
} elseif ($scenario === 'upgrade') {
$GLOBALS['__test_config']['flowview_nbar_schema_version'] = '0';
- $_SERVER['PHP_SELF'] = 'fixture.php';
+ $_SERVER['PHP_SELF'] = 'fixture.php';
} elseif ($scenario === 'more-pages') {
$_REQUEST['mode'] = 'connections';
- $from = (int) floor(time() / 3600) * 3600 - 3600;
+ $from = (int) floor(time() / 3600) * 3600 - 3600;
$_REQUEST += ['predefined_timespan' => '0', 'date1' => date('Y-m-d H:i', $from), 'date2' => date('Y-m-d H:i', $from + 1200), 'lock_range' => '1'];
+
foreach (range(1, 4) as $unused) {
plugin_test_queue_db_result('db_fetch_assoc_prepared', []);
}
$rows = [];
+
foreach (range(1, 101) as $i) {
$rows[] = ['app_id' => '0d000001', 'src_hex' => bin2hex(inet_pton('192.0.2.' . $i)), 'dst_hex' => bin2hex(inet_pton('198.51.100.20')), 'sport' => $i, 'dport' => 443, 'protocol' => 6, 'bytes' => '10', 'packets' => '1', 'records' => '1', 'first_seen' => $from, 'last_seen' => $from + 60];
}
@@ -139,6 +146,7 @@ function bottom_footer(): void {
$_REQUEST += ['prefs_apply' => '1', 'local_prefixes' => ''];
$GLOBALS['__test_csrf_valid'] = false;
}
+
foreach (range(1, 3) as $unused) {
plugin_test_queue_db_result('db_fetch_assoc', []);
}
diff --git a/tests/helpers/request.php b/tests/helpers/request.php
index d42ad3ca..de8fa898 100644
--- a/tests/helpers/request.php
+++ b/tests/helpers/request.php
@@ -1,14 +1,16 @@
toBe(0);
expect($stderr)->toBe('');
- $result = json_decode(file_get_contents($directory . '/result.json'), true, 512, JSON_THROW_ON_ERROR);
+ $result = json_decode(file_get_contents($directory . '/result.json'), true, 512, JSON_THROW_ON_ERROR);
$coverage = \PHPUnit\Runner\CodeCoverage::instance();
+
if ($result['coverage'] && $coverage->isActive()) {
$coverage->codeCoverage()->append(\SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData::fromXdebugWithoutPathCoverage($result['coverage']));
}
+
return [$stdout, $result];
}
From 62dcd256f3095621f60bc38f282105bcd81b2f31 Mon Sep 17 00:00:00 2001
From: TheWitness
Date: Mon, 19 Oct 2026 12:41:03 -0400
Subject: [PATCH 19/27] fix: guard the background database upgrade with a
process registration
plugin_flowview_check_upgrade() runs on every page load while an upgrade is pending, so each request could spawn another background upgrade. Launch it only when register_process_start() succeeds (four hour timeout). cli/flowview_upgrade.php releases that lock, and its own run lock, however it ends (early exit, success, or fatal error via a shutdown function); its own registration timeout drops from 24 hours to 4 hours to match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
cli/flowview_upgrade.php | 13 ++++++++++---
setup.php | 6 +++++-
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/cli/flowview_upgrade.php b/cli/flowview_upgrade.php
index 0045a121..1c969820 100644
--- a/cli/flowview_upgrade.php
+++ b/cli/flowview_upgrade.php
@@ -105,22 +105,29 @@
/* do a version check */
if ($forcever == '' && $old_version == $current_version && (int) read_config_option('flowview_nbar_schema_version') >= 2) {
cacti_log('Your Flowview is already up to date (v' . $current_version . ' vs v' . $old_version . ') not upgrading. Use --forcever to override', true, 'FLOWVIEW');
+
+ unregister_process('flowview', 'upgrade_launch', 0);
+
exit(0);
}
-if (!register_process_start('flowview', 'upgrade', 1, 86400)) {
+if (!register_process_start('flowview', 'upgrade', 1, 14400)) {
if (empty($forcever)) {
print 'WARNING: Running process detected. To override use --forcever' . PHP_EOL;
exit(1);
}
}
+// release this run and the web launcher's lock however the script ends, including a fatal error
+register_shutdown_function(static function (): void {
+ unregister_process('flowview', 'upgrade', 1);
+ unregister_process('flowview', 'upgrade_launch', 0);
+});
+
cacti_log('Upgrading from v' . $old_version . ' to ' . $current_version, true, 'FLOWVIEW');
flowview_upgrade($current_version, $old_version);
-unregister_process('flowview', 'upgrade', 1);
-
exit(0);
/**
diff --git a/setup.php b/setup.php
index 6c18e4fc..ef321e4e 100644
--- a/setup.php
+++ b/setup.php
@@ -166,7 +166,11 @@ function plugin_flowview_check_upgrade(bool $force = false): void {
if ($current != $old || $force || (int) read_config_option('flowview_nbar_schema_version') < 2) {
$php_binary = read_config_option('path_php_binary');
- exec_background($php_binary, '-q ' . $config['base_path'] . '/plugins/flowview/cli/flowview_upgrade.php');
+ /* This check runs on every page load. Launch the upgrade once; cli/flowview_upgrade.php releases the
+ * lock when it finishes, and the four hour timeout recovers it if that process dies. */
+ if (register_process_start('flowview', 'upgrade_launch', 0, 14400)) {
+ exec_background($php_binary, '-q ' . $config['base_path'] . '/plugins/flowview/cli/flowview_upgrade.php');
+ }
raise_message('flowview_upgrade', __('Please be advised the Flowview plugins Tables are being upgraded in the background. This may take some time. Check the Cacti log for more information'), MESSAGE_LEVEL_INFO);
From 22a4fed274a3c59360a193caeadd20a6998bdde4 Mon Sep 17 00:00:00 2001
From: TheWitness
Date: Mon, 19 Oct 2026 12:41:04 -0400
Subject: [PATCH 20/27] refactor: use cacti_count()/cacti_sizeof(); fix PHPStan
level 8 findings in NBAR code
- Replace count()/sizeof() with cacti_count()/cacti_sizeof() in production code (Net/ excluded); add a cacti_count stub to the unit bootstrap.
- Narrow-width fv_nbar_int() for NetFlow header fields; typed/guarded results for unpack, preg_split, inet_pton/inet_ntop, hex2bin, json_encode, parse_url.
- Declare variables inherited by the included report fragments, initialise by-reference arguments, and pass correctly typed html_start_box() arguments.
- Remove an always-true condition in fv_section_params().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
flowview_sections.php | 12 ++++------
includes/functions.php | 2 +-
includes/nbar_collector.php | 44 ++++++++++++++++++++++-------------
includes/nbar_common.php | 11 +++++++--
includes/nbar_connections.php | 6 ++---
includes/nbar_database.php | 1 +
includes/nbar_preferences.php | 6 ++---
includes/nbar_timespan.php | 2 +-
nbar_conversations.php | 16 +++++++++++++
nbar_report.php | 16 +++++++++----
nbar_setup_guide.php | 1 +
tests/bootstrap-unit.php | 6 +++++
12 files changed, 86 insertions(+), 37 deletions(-)
diff --git a/flowview_sections.php b/flowview_sections.php
index d21afa0f..7f05718e 100644
--- a/flowview_sections.php
+++ b/flowview_sections.php
@@ -64,14 +64,12 @@ function fv_section_params(string $section): array {
}
$p['predefined_timespan'] = 1; // Re-enter reports with a fresh half-hour window.
- if ((int) ($p['predefined_timespan'] ?? 0) > 0) {
- unset($p['date1'],$p['date2']);
- $c = json_decode($p['context'] ?? '', true);
+ unset($p['date1'],$p['date2']);
+ $c = json_decode($p['context'] ?? '', true);
- if (is_array($c)) {
- unset($c['date1'],$c['date2']);
- $p['context'] = json_encode($c);
- }
+ if (is_array($c)) {
+ unset($c['date1'],$c['date2']);
+ $p['context'] = json_encode($c);
}
$p['action'] = 'view';
$p['section'] = $section;
diff --git a/includes/functions.php b/includes/functions.php
index bbe9f3f0..2a5d73e0 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -2832,7 +2832,7 @@ function run_flow_query($session, $query_id, $start, $end) {
$data['printed'] = $nreport;
$data['statistics'] = 0;
}
- } elseif (!sizeof($data)) {
+ } elseif (!cacti_sizeof($data)) {
return false;
}
diff --git a/includes/nbar_collector.php b/includes/nbar_collector.php
index 1f4c2d2d..6121289e 100644
--- a/includes/nbar_collector.php
+++ b/includes/nbar_collector.php
@@ -51,7 +51,7 @@ public function remove(string $router, string $source, int $id): void {
public function store(string $router, string $source, int $id, array $template, int $now): bool {
$this->prune($now);
$this->remove($router, $source, $id);
- $fields = count($template[0]);
+ $fields = cacti_count($template[0]);
if ($fields > 2048) {
return false;
@@ -97,10 +97,10 @@ public function get(string $router, string $source, int $id, int $now): ?array {
}
public function usage(string $router): array {
- $usage = ['templates' => count($this->entries), 'fields' => 0, 'exporter_templates' => 0, 'exporter_fields' => 0];
+ $usage = ['templates' => cacti_count($this->entries), 'fields' => 0, 'exporter_templates' => 0, 'exporter_fields' => 0];
foreach ($this->entries as $entry) {
- $fields = count($entry['template'][0]);
+ $fields = cacti_count($entry['template'][0]);
$usage['fields'] += $fields;
if ($entry['router'] === $router) {
@@ -126,9 +126,9 @@ function fv_nbar_uint(string $s): int|string {
}
$n = 0;
- foreach (unpack('C*', $s) as $v) {
+ foreach (unpack('C*', $s) ?: [] as $v) {
if (is_int($n) && $n <= intdiv(PHP_INT_MAX - $v, 256)) {
- $n = $n * 256 + $v;
+ $n = (int) ($n * 256 + $v);
continue;
}
@@ -146,6 +146,18 @@ function fv_nbar_uint(string $s): int|string {
return $n;
}
+
+/**
+ * Decode a network-order integer of at most four bytes, which always fits a native int.
+ *
+ * @param string $s Network-order bytes.
+ *
+ * @return int
+ */
+function fv_nbar_int(string $s): int {
+ return (int) fv_nbar_uint($s);
+}
+
/**
* Decode template-defined NetFlow v9 application records and names.
*
@@ -158,22 +170,22 @@ function fv_nbar_packet(string $p, string $router): void {
global $listener_id;
static $templates = null;
- if (strlen($p) < 20 || fv_nbar_uint(substr($p, 0, 2)) !== 9) {
+ if (strlen($p) < 20 || fv_nbar_int(substr($p, 0, 2)) !== 9) {
return;
}
$templates ??= new FlowviewNbarTemplateCache();
$now = time();
$templates->prune($now);
$source = bin2hex(substr($p, 16, 4));
- $unix = fv_nbar_uint(substr($p, 8, 4));
- $uptime = fv_nbar_uint(substr($p, 4, 4));
+ $unix = fv_nbar_int(substr($p, 8, 4));
+ $uptime = fv_nbar_int(substr($p, 4, 4));
$rows = [];
$args = [];
$hours = [];
for ($o = 20; $o + 4 <= strlen($p);) {
- $id = fv_nbar_uint(substr($p, $o, 2));
- $len = fv_nbar_uint(substr($p, $o + 2, 2));
+ $id = fv_nbar_int(substr($p, $o, 2));
+ $len = fv_nbar_int(substr($p, $o + 2, 2));
if ($len < 4 || $o + $len > strlen($p)) {
break;
@@ -183,18 +195,18 @@ function fv_nbar_packet(string $p, string $router): void {
if ($id === 0 || $id === 1) {
while ($q + ($id === 0 ? 4 : 6) <= $end) {
- $tid = fv_nbar_uint(substr($p, $q, 2));
+ $tid = fv_nbar_int(substr($p, $q, 2));
if ($tid < 256) {
break;
}
if ($id === 0) {
- $count = fv_nbar_uint(substr($p, $q + 2, 2));
+ $count = fv_nbar_int(substr($p, $q + 2, 2));
$size = $count * 4;
$q += 4;
} else {
- $size = fv_nbar_uint(substr($p, $q + 2, 2)) + fv_nbar_uint(substr($p, $q + 4, 2));
+ $size = fv_nbar_int(substr($p, $q + 2, 2)) + fv_nbar_int(substr($p, $q + 4, 2));
$q += 6;
}
@@ -208,8 +220,8 @@ function fv_nbar_packet(string $p, string $router): void {
$valid = true;
for ($z = 0; $z < $size; $z += 4) {
- $f = fv_nbar_uint(substr($p, $q + $z, 2));
- $l = fv_nbar_uint(substr($p, $q + $z + 2, 2));
+ $f = fv_nbar_int(substr($p, $q + $z, 2));
+ $l = fv_nbar_int(substr($p, $q + $z + 2, 2));
$width = [1 => 8, 2 => 8, 4 => 1, 7 => 2, 11 => 2, 21 => 4][$f] ?? null;
if ($l === 0 || ($id === 0 && $width !== null && $l > $width)) {
@@ -262,7 +274,7 @@ function fv_nbar_packet(string $p, string $router): void {
$ts = $unix;
if (isset($data[21])) {
- $age = ($uptime - fv_nbar_uint($data[21]) + 4294967296) % 4294967296;
+ $age = ($uptime - fv_nbar_int($data[21]) + 4294967296) % 4294967296;
$ts = $unix - (int) floor($age / 1000);
}
diff --git a/includes/nbar_common.php b/includes/nbar_common.php
index 4e43bf1f..cc293802 100644
--- a/includes/nbar_common.php
+++ b/includes/nbar_common.php
@@ -142,7 +142,7 @@ function fv_nbar_supervise_maintenance(): void {
try {
// Invoke PHP directly; older Cacti timeout helpers accept strings only.
- $command = [read_config_option('path_php_binary'), '-q', $config['base_path'] . '/plugins/flowview/cli/flowview_upgrade.php', '--nbar-worker=build'];
+ $command = [(string) read_config_option('path_php_binary'), '-q', $config['base_path'] . '/plugins/flowview/cli/flowview_upgrade.php', '--nbar-worker=build'];
$status = fv_nbar_run_child($command, 90);
if ($status !== 0) {
@@ -153,7 +153,14 @@ function fv_nbar_supervise_maintenance(): void {
}
}
-/** Supervise a direct PHP child independently of Cacti's versioned command API. */
+/**
+ * Supervise a direct PHP child independently of Cacti's versioned command API.
+ *
+ * @param list $command Executable followed by its arguments.
+ * @param int $seconds Hard process deadline in seconds.
+ *
+ * @return int
+ */
function fv_nbar_run_child(array $command, int $seconds): int {
$pipes = [];
$process = false;
diff --git a/includes/nbar_connections.php b/includes/nbar_connections.php
index bb85a7b4..d641717e 100644
--- a/includes/nbar_connections.php
+++ b/includes/nbar_connections.php
@@ -69,7 +69,7 @@ function fv_nbar_decimal_compare(int|string $a, int|string $b): int {
* @return string
*/
function fv_nbar_connection_key(array $row, array $names, int $listener): string {
- return json_encode([(string) $row['app_id'], (int) ($names[$listener][($row['router'] ?? '') . ':' . $row['app_id']]['name_id'] ?? 0), $row['src_hex'], $row['dst_hex'], (int) $row['sport'], (int) $row['dport'], (int) $row['protocol'], (string) ($row['router'] ?? '')]);
+ return json_encode([(string) $row['app_id'], (int) ($names[$listener][($row['router'] ?? '') . ':' . $row['app_id']]['name_id'] ?? 0), $row['src_hex'], $row['dst_hex'], (int) $row['sport'], (int) $row['dport'], (int) $row['protocol'], (string) ($row['router'] ?? '')], JSON_THROW_ON_ERROR);
}
/**
* Order connection totals by exact byte count and deterministic tuple fields.
@@ -349,8 +349,8 @@ function fv_nbar_connections_direct(array $listeners, int $start, int $end, stri
$result = array_slice($ranked, $offset, $size);
foreach ($result as &$row) {
- $row['src'] = inet_ntop(hex2bin($row['src_hex']));
- $row['dst'] = inet_ntop(hex2bin($row['dst_hex']));
+ $row['src'] = inet_ntop((string) hex2bin($row['src_hex']));
+ $row['dst'] = inet_ntop((string) hex2bin($row['dst_hex']));
unset($row['_key'],$row['src_hex'],$row['dst_hex']);
} unset($row);
flowview_db_execute('COMMIT');
diff --git a/includes/nbar_database.php b/includes/nbar_database.php
index 0ce1bcd2..7cf4e0da 100644
--- a/includes/nbar_database.php
+++ b/includes/nbar_database.php
@@ -115,6 +115,7 @@ function fv_nbar_schema_definition(string $suffix): array {
$primary[] = 'router';
$columns[] = 'router';
}
+ /** @var array> $fields */
$fields = array_merge($fields, $extra);
$definition = ['columns' => [], 'primary' => implode('`,`', $primary), 'keys' => $keys, 'type' => 'InnoDB'];
diff --git a/includes/nbar_preferences.php b/includes/nbar_preferences.php
index b8dc7357..e9e459c5 100644
--- a/includes/nbar_preferences.php
+++ b/includes/nbar_preferences.php
@@ -34,13 +34,13 @@
function fv_nbar_cidrs(string $text): array|false {
$out = [];
- foreach (preg_split('/[\s,]+/', trim($text), -1, PREG_SPLIT_NO_EMPTY) as $cidr) {
+ foreach (preg_split('/[\s,]+/', trim($text), -1, PREG_SPLIT_NO_EMPTY) ?: [] as $cidr) {
$parts = explode('/', $cidr);
if (cacti_sizeof($parts) !== 2 || !ctype_digit($parts[1]) || !filter_var($parts[0], FILTER_VALIDATE_IP)) {
return false;
}
- $packed = inet_pton($parts[0]);
+ $packed = (string) inet_pton($parts[0]);
$bits = (int) $parts[1];
if ($bits < 0 || $bits > strlen($packed) * 8) {
@@ -128,7 +128,7 @@ function fv_nbar_local_sql(string $f): string {
foreach ($GLOBALS['fv_nbar_preferences']['prefixes'] as $cidr) {
[$ip,$bits] = explode('/', $cidr);
- $packed = inet_pton($ip);
+ $packed = (string) inet_pton($ip);
$bits = (int) $bits;
$whole = intdiv($bits, 8);
$rest = $bits % 8;
diff --git a/includes/nbar_timespan.php b/includes/nbar_timespan.php
index 1d41b552..fc44a45b 100644
--- a/includes/nbar_timespan.php
+++ b/includes/nbar_timespan.php
@@ -39,7 +39,7 @@ function fv_nbar_timespan_options(array $presets, int $now, int $days, int|strin
foreach ($presets as $id => $label) {
$span = [];
- get_timespan($span, $now, $id, $weekday);
+ get_timespan($span, $now, $id, (int) $weekday);
$from = strtotime($span['current_value_date1']);
$to = strtotime($span['current_value_date2']);
diff --git a/nbar_conversations.php b/nbar_conversations.php
index 16357e53..a641eb0e 100644
--- a/nbar_conversations.php
+++ b/nbar_conversations.php
@@ -24,6 +24,22 @@
*/
// Conversation direction uses configured site networks and selected listeners.
+
+/**
+ * Report state supplied by nbar_report.php, which includes this file.
+ *
+ * @var array $nbar_preferences
+ * @var string $listener_scope
+ * @var int $lid
+ * @var int $start
+ * @var int $end
+ * @var int $seconds
+ * @var string $family
+ * @var string $aid
+ * @var string $host
+ * @var string $base
+ * @var array $combined_lids
+ */
if (empty($nbar_preferences['prefixes'])) {
print '
' . html_escape(__('Configure your site networks in Application settings to label internal and external devices. Top applications and Connections work directly with exported IPv4 and IPv6 addresses.', 'flowview')) . '
';
diff --git a/nbar_report.php b/nbar_report.php
index a9781e02..2f8f5b23 100644
--- a/nbar_report.php
+++ b/nbar_report.php
@@ -42,6 +42,13 @@
// arrays.php populates report variables in this scope, so it is included on each request.
include(__DIR__ . '/includes/arrays.php');
+/**
+ * Report arrays supplied by Cacti core and includes/arrays.php.
+ *
+ * @var array $graph_timespans
+ * @var array> $stat_report_array
+ * @var array> $print_report_array
+ */
if ((int) read_config_option('flowview_nbar_schema_version') < 2) {
plugin_flowview_check_upgrade();
print '
' . html_escape(__('NBAR history is being upgraded. Refresh this report shortly.', 'flowview')) . '