Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# review pipeline scratch
.diffcheck.php
.rv.tar
tests/_dbg.php
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## Changelog

--- 2.2 ---

* Add IPv6 map and subnet drill-down support with filename-safe prefix tokens
* Move hostname resolution to an asynchronous, last-known-good DNS cache
* Surface active thold alarms on otherwise-up Device markers
* Prune generated subnet artefacts after three poller intervals

--- 2.1 ---

* issue: Add gpsmap_templates table index to be unique by device template
Expand Down
2 changes: 1 addition & 1 deletion INFO
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[info]
name = gpsmap
version = 2.1
version = 2.2
longname = Maps Plugin
author = Andrew Aloia, Wixiweb, The Cacti Group
email =
Expand Down
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ Maps tab reads a static file instead of querying the database.

## Features

* Devices plotted by latitude and longitude, coloured by Up, Recovering, Down,
Disabled or Undefined status
* IPv4 and IPv6 Devices plotted by latitude and longitude, coloured by Up,
Recovering, Down, Disabled or Undefined status. When thold is enabled, an
otherwise-up Device with an active threshold uses its Down icon

* Per Device Template marker icons for the Up, Recovering and Down states

* Coverage overlay drawing an area around Access Points, sized to reach the
furthest Device sharing the Access Point's group number, and optionally
limited to an arc

* Drill down by subnet, from the first octet through to individual Devices
* Drill down by subnet: /8, /16 and /24 for IPv4, and /16, /32 and /48 for IPv6

* KML export alongside the XML, so the same data opens in Google Earth

Expand Down Expand Up @@ -55,8 +56,8 @@ JavaScript, and there is no workaround inside Cacti.

Installing adds seven columns to Cacti's `host` table (`latitude`, `longitude`,
`GPScoverage`, `start`, `stop`, `groupnum`, `rdistance`) and creates the
`gpsmap_templates` table. Uninstalling deliberately leaves both in place so
coordinates survive an accidental removal.
`gpsmap_templates` and `plugin_gpsmap_dns_cache` tables. Uninstalling
deliberately leaves them in place so coordinates survive an accidental removal.

## Configuration

Expand Down Expand Up @@ -139,13 +140,21 @@ Understanding the order of events explains most of what can go wrong.
1. The poller runs and Cacti calls the plugin's `poller_bottom` hook.

2. The plugin selects every Device whose Device Template appears in
`gpsmap_templates`, resolves each hostname to an address, and discards any
Device without coordinates or whose name does not resolve to IPv4.
`gpsmap_templates`. Literal IPv4 and IPv6 addresses are used directly;
configured names use the last address in `plugin_gpsmap_dns_cache`.

3. It writes `plugins/gpsmap/XML/all.xml`, `all.kml` and `all-top.html`, then
repeats for each subnet prefix so the drill-down views are prebuilt.

4. Opening the Maps tab serves a page that loads Google's map library and
4. It starts a separate DNS refresh worker after publishing. A newly configured
hostname can therefore appear on the following poll, while slow DNS never
holds up the poller. A failed refresh retains the last-known address.

5. Generated subnet files not refreshed for three poller intervals are removed.
Cleanup runs only after a successful Device query; unrelated files are never
touched.

6. Opening the Maps tab serves a page that loads Google's map library and
fetches the matching `.xml` file over HTTP.

Nothing appears until a poll cycle has completed after configuration. If
Expand All @@ -165,9 +174,14 @@ cycle has completed since both were set; and `plugins/gpsmap/XML` contains
files newer than the last poll.

**Some Devices are missing.** A Device is skipped when it has no coordinates,
when its hostname does not resolve to an IPv4 address, when its Device Template
is not in Map Templates, or when it is disabled and Display Disabled Devices is
off.
when its hostname has no cached address yet, when its Device Template is not in
Map Templates, or when it is disabled and Display Disabled Devices is off. For
a newly added hostname, allow one additional poll cycle for the asynchronous DNS
worker to populate the cache.

**An Up Device has a red marker.** When the thold plugin is enabled, gpsmap
uses the configured Down icon for an otherwise-up Device with any active,
enabled threshold. Disable or clear the threshold to restore the Up icon.

**The XML directory is empty.** The Data Collector cannot write to it. Check
ownership, permissions and SELinux context. The Cacti log records a failure
Expand Down
56 changes: 28 additions & 28 deletions gpsmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@

general_header();

//set headers to NOT cache a page
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
// set headers to NOT cache a page
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

//decide what needs to be shown
// decide what needs to be shown
switch ($show) {
//selected nodes
// selected nodes
case 'setup':
$body = __('Please make sure to properly configure Maps first under Settings > Maps', 'gpsmap');

break;
default:
if (!$parameter) {
Expand All @@ -46,44 +47,44 @@
/* Reject any parameter that contains directory traversal sequences or
* characters outside the safe set. basename() alone does not strip
* embedded ../ so we validate the whole value first. */
if (!preg_match('/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$/', $parameter)) {
if (!preg_match('/^(?:all|[0-9]+(?:\.[0-9]+){0,2}|v6-(?:16-[0-9a-f]{4}|32-[0-9a-f]{8}|48-[0-9a-f]{12}))$/', $parameter)) {
$parameter = 'all';
}

$fileLocation = './plugins/gpsmap/XML/' . $parameter . '-top.html';

if (file_exists($fileLocation)) {
echo file_get_contents($fileLocation);
print file_get_contents($fileLocation);
}

break;
}

//---------------------------------------------------------------
// ---------------------------------------------------------------

if ($show != 'setup') { ?>
<script type='text/javascript'>
var initialLat = <?php echo is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
var initialLng = <?php echo is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
var initialZoom = <?php echo json_encode((int) $initialzoom); ?>;
var initialLat = <?php print is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
var initialLng = <?php print is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
var initialZoom = <?php print json_encode((int) $initialzoom); ?>;
</script>
<script type='text/javascript'>
gpsmap.refreshMap = <?php echo json_encode((string) $refreshMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.initialLat = <?php echo is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
gpsmap.initialLng = <?php echo is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
gpsmap.initialZoom = <?php echo json_encode((int) $initialzoom); ?>;
gpsmap.refreshMap = <?php print json_encode((string) $refreshMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.initialLat = <?php print is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
gpsmap.initialLng = <?php print is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
gpsmap.initialZoom = <?php print json_encode((int) $initialzoom); ?>;
gpsmap.initialized = false;
gpsmap.liColor = <?php echo json_encode((string) $liColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liWidth = <?php echo json_encode((string) $liWidth, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liOpa = <?php echo json_encode((string) $liOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillColor = <?php echo json_encode((string) $fillColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillOpa = <?php echo json_encode((string) $fillOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.circleQuality = <?php echo json_encode((string) $circleQuality, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.enableWeather = <?php echo json_encode((string) $enableWeather, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.coverageOverlay = <?php echo json_encode((string) $coverageMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liColor = <?php print json_encode((string) $liColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liWidth = <?php print json_encode((string) $liWidth, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liOpa = <?php print json_encode((string) $liOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillColor = <?php print json_encode((string) $fillColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillOpa = <?php print json_encode((string) $fillOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.circleQuality = <?php print json_encode((string) $circleQuality, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.enableWeather = <?php print json_encode((string) $enableWeather, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.coverageOverlay = <?php print json_encode((string) $coverageMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.markerArray = [];
gpsmap.downloadURL = <?php echo json_encode($config['url_path'] . 'plugins/gpsmap/XML/' . $parameter . '.xml', JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.t_error = <?php echo is_finite((float) $terror) ? json_encode((float) $terror) : '0'; ?>;
gpsmap.downloadURL = <?php print json_encode($config['url_path'] . 'plugins/gpsmap/XML/' . $parameter . '.xml', JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.t_error = <?php print is_finite((float) $terror) ? json_encode((float) $terror) : '0'; ?>;

<?php include_once('plugins/gpsmap/includes/icons.php'); ?>
<?php include_once('plugins/gpsmap/includes/customicons.php'); ?>
Expand All @@ -101,7 +102,6 @@
<div id="mouse-position"></div>
<div id="map"></div>
';
echo $body;
print $body;

bottom_footer();

21 changes: 21 additions & 0 deletions gpsmap_dns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/

chdir(__DIR__ . '/../../');
require_once('./include/cli_check.php');
require_once($config['base_path'] . '/plugins/gpsmap/includes/dns.php');

if (!register_process_start('gpsmap', 'dns-refresh', 0, 3600)) {
exit(0);
}

try {
gpsmap_refresh_dns_cache();
} finally {
unregister_process('gpsmap', 'dns-refresh', 0);
}
38 changes: 38 additions & 0 deletions gpsmap_security.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,41 @@ function gpsmap_normalize_icon_name($value, $icon_array, $default = 'Green.png')

return $value;
}

//---------------------------------------------------------------
function getIcons() {
$iconArray = array();
global $config;

/* Built from base_path so poller and CLI callers resolve it too; only web
* entry points chdir() to the Cacti root. */
$dir = $config['base_path'] . '/plugins/gpsmap/images/icons';

/* Suppressed rather than warned: this runs on the Map Templates page, and a
* missing icon folder should not print a PHP warning into the form. */
$icons = @opendir($dir);

if ($icons === false) {
return $iconArray;
}

while (false !== ($icon = readdir($icons))) {
/* Offer only names the map can actually render. icons.php emits each
* base name as a JavaScript identifier, so a file this rule rejects
* would appear in the dropdown, save cleanly, and then silently fail
* to draw. */
if (!in_array(strtolower(pathinfo($icon, PATHINFO_EXTENSION)), GPSMAP_ICON_EXTENSIONS, true)) {
continue;
}

if (gpsmap_icon_identifier($icon) === null) {
continue;
}

$iconArray[$icon] = $icon;
}

closedir($icons);

return $iconArray;
}
28 changes: 1 addition & 27 deletions gpstemplates.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function templates() {
$url = $config['url_path'] . 'plugins/gpsmap/gpstemplates.php?action=edit&id=' . $template['templateID'];

form_alternate_row('line' . $template['templateID'], true);
form_selectable_cell("<a class='linkEditMain' href='$url'>" . html_escape($template['templateName']) . "</a>", $template['templateID']);
form_selectable_cell("<a class='linkEditMain' href='" . html_escape($url) . "'>" . html_escape($template['templateName']) . "</a>", $template['templateID']);
form_selectable_cell("<img src='" . $config['url_path'] . 'plugins/gpsmap/images/icons/' . html_escape($template['upimage']) . "'>", $template['templateID']);
form_selectable_cell("<img src='" . $config['url_path'] . 'plugins/gpsmap/images/icons/' . html_escape($template['recoverimage']) . "'>", $template['templateID']);
form_selectable_cell("<img src='" . $config['url_path'] . 'plugins/gpsmap/images/icons/' . html_escape($template['downimage']) . "'>", $template['templateID']);
Expand Down Expand Up @@ -252,29 +252,3 @@ function gpsmap_save_template() {
}

//------------------------------------------------------------------------------
function getIcons() {
$iconArray = array();
$icons = opendir('./plugins/gpsmap/images/icons');

while (false !== ($icon = readdir($icons))) {
if ($icon != '.' && $icon != '..') {
$iconExplode = explode('.', $icon);
$iconExplode[1] = $iconExplode[1];

switch ($iconExplode[1]) {
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
$iconArray[$icon] = $icon;
break;
default:
break;
}
}
}

closedir($icons);

return $iconArray;
}
20 changes: 11 additions & 9 deletions includes/customicons.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
*/

require_once(__DIR__ . '/../gpsmap_security.php');
//This file takes care of the individual assignments for each custom icon set for the map templates
//This pulls the template from MySQL and attaches the Up/Down/Recover icon reference.
//Basically this makes a mapping between template and icon.
// This file takes care of the individual assignments for each custom icon set for the map templates
// This pulls the template from MySQL and attaches the Up/Down/Recover icon reference.
// Basically this makes a mapping between template and icon.

$customiconlist = "gpsmap.customIcons = {};\n";

Expand All @@ -32,26 +32,28 @@ function gpsmap_safe_icon_base(string $filename): string {
return gpsmap_icon_identifier($filename) ?? 'undefined';
}

$results = db_fetch_assoc_prepared('SELECT * FROM gpsmap_templates ORDER BY templateID', array());
$results = db_fetch_assoc_prepared('SELECT * FROM gpsmap_templates ORDER BY templateID', []);

if (cacti_sizeof($results)) {
foreach ($results as $row) {
$tid = (int) $row['templateID'];
$up_base = gpsmap_safe_icon_base($row['upimage']);
$down_base = gpsmap_safe_icon_base($row['downimage']);
$recover_base = gpsmap_safe_icon_base($row['recoverimage']);

$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'up') . '] = gpsmap.' . $up_base . ";\n";
$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'down') . '] = gpsmap.' . $down_base . ";\n";
$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'up') . '] = gpsmap.' . $up_base . ";\n";
$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'down') . '] = gpsmap.' . $down_base . ";\n";
$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'recovering') . '] = gpsmap.' . $recover_base . ";\n";
$customiconlist .= 'gpsmap.customIcons[' . json_encode($tid . 'alert') . '] = gpsmap.' . $down_base . ";\n";
}
}

//DO NOT REMOVE THESE
// DO NOT REMOVE THESE
$customiconlist .= "gpsmap.customIcons['up'] = gpsmap.Green;\n";
$customiconlist .= "gpsmap.customIcons['recovering'] = gpsmap.Orange;\n";
$customiconlist .= "gpsmap.customIcons['alert'] = gpsmap.Red;\n";
$customiconlist .= "gpsmap.customIcons['down'] = gpsmap.Red;\n";
$customiconlist .= "gpsmap.customIcons['disabled'] = gpsmap.Black;\n";
$customiconlist .= "gpsmap.customIcons['undefined'] = gpsmap.Black;\n";

echo $customiconlist;

print $customiconlist;
Loading