mirror of
https://github.com/itflow-org/itflow
synced 2026-08-26 17:35:16 +00:00
Revised the network info sidebar, added ip count, prefixed subnet IP in form, ajax check within network
This commit is contained in:
@@ -913,6 +913,47 @@ if (isset($_GET['client_duplicate_check'])) {
|
||||
echo json_encode($response);
|
||||
}
|
||||
|
||||
/*
|
||||
* Live check behind the IP address field on the add/edit network IP modals.
|
||||
*
|
||||
* Runs the exact same checkIpForNetwork() the POST handler runs, so what the
|
||||
* field says while you type is what the save will do - no second copy of the
|
||||
* rules to drift. It's advisory only; the handler still checks on submit.
|
||||
*/
|
||||
if (isset($_GET['network_ip_check'])) {
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$network_id = intval($_GET['network_id'] ?? 0);
|
||||
$ip_id = intval($_GET['ip_id'] ?? 0);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$ip = $_GET['ip'] ?? '';
|
||||
|
||||
$response = ['ok' => false, 'ip' => '', 'message' => ''];
|
||||
|
||||
if (trim($ip) !== '') {
|
||||
|
||||
$ip_error = checkIpForNetwork($ip, $network_id, $ip_id);
|
||||
|
||||
if ($ip_error === '') {
|
||||
$response['ok'] = true;
|
||||
$response['ip'] = $ip;
|
||||
$response['message'] = "<i class='fas fa-fw fa-check me-2'></i>" . escapeHtml($ip) . " is available";
|
||||
} else {
|
||||
// checkIpForNetwork() builds its message for flashAlert(), which
|
||||
// escapes at render - this lands in innerHTML instead, so it has
|
||||
// to be escaped here
|
||||
$response['message'] = "<i class='fas fa-fw fa-exclamation-triangle me-2'></i>" . alertMessageHtml($ip_error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
}
|
||||
|
||||
if (isset($_GET['contact_email_check'])) {
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ $client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// The part of the address this subnet fixes. Empty for IPv6, a prefix under
|
||||
// /8, or a subnet that won't parse - then the field takes a whole address.
|
||||
$ip_fixed_octets = ipSubnetFixedOctets($row['network']);
|
||||
$ip_host_octets = $ip_fixed_octets ? 4 - substr_count($ip_fixed_octets, '.') : 0;
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -31,9 +36,19 @@ ob_start();
|
||||
<div class="mb-3">
|
||||
<label>IP Address <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<?php if ($ip_fixed_octets) { ?>
|
||||
<span class="input-group-text font-monospace text-bold"><?= escapeHtml($ip_fixed_octets) ?></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" id="networkIpAddress"
|
||||
placeholder="<?= $ip_host_octets === 1 ? '10' : str_repeat('0.', $ip_host_octets - 1) . '10' ?>"
|
||||
<?= $ip_host_octets === 1 ? 'inputmode="numeric"' : '' ?>
|
||||
maxlength="<?= ($ip_host_octets * 4) - 1 ?>" required autofocus>
|
||||
<?php } else { ?>
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-pin"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" placeholder="Must be inside <?= $network ?>" maxlength="45" required autofocus>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" id="networkIpAddress"
|
||||
placeholder="Must be inside <?= $network ?>" maxlength="45" required autofocus>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="small mt-1" id="networkIpFeedback"></div>
|
||||
<small class="text-secondary"><?= $network_name ?> — <span class="font-monospace"><?= $network ?></span></small>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +75,12 @@ ob_start();
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Live version of the duplicate / inside-the-subnet checks. Advisory only -
|
||||
// agent/post/network_ip.php runs the same checks again on submit.
|
||||
itflowWatchNetworkIp(<?= $network_id ?>, 0);
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -11,6 +11,7 @@ $sql = mysqli_query($mysqli, "SELECT ip_address, ip_description, ip_hostname, ip
|
||||
WHERE ip_id = $ip_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$network_id = intval($row['ip_network_id']);
|
||||
$ip_address = escapeHtml($row['ip_address']);
|
||||
$ip_hostname = escapeHtml($row['ip_hostname']);
|
||||
$ip_description = escapeHtml($row['ip_description']);
|
||||
@@ -20,6 +21,14 @@ $client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Same split as the add modal - the fixed octets are shown, the host part is
|
||||
// what's editable. An address that predates the subnet (or sits outside it)
|
||||
// won't match the prefix and is shown in full so it can still be corrected.
|
||||
$ip_fixed_octets = ipSubnetFixedOctets($row['network']);
|
||||
$ip_host_octets = $ip_fixed_octets ? 4 - substr_count($ip_fixed_octets, '.') : 0;
|
||||
$ip_host_value = escapeHtml(ipSuffixForDisplay($row['ip_address'], $row['network']));
|
||||
$ip_prefix_matched = ($ip_fixed_octets !== '' && $ip_host_value !== $ip_address);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -36,9 +45,19 @@ ob_start();
|
||||
<div class="mb-3">
|
||||
<label>IP Address <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<?php if ($ip_prefix_matched) { ?>
|
||||
<span class="input-group-text font-monospace text-bold"><?= escapeHtml($ip_fixed_octets) ?></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" id="networkIpAddress"
|
||||
value="<?= $ip_host_value ?>"
|
||||
<?= $ip_host_octets === 1 ? 'inputmode="numeric"' : '' ?>
|
||||
maxlength="<?= ($ip_host_octets * 4) - 1 ?>" required>
|
||||
<?php } else { ?>
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-pin"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" value="<?= $ip_address ?>" maxlength="45" required>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" id="networkIpAddress"
|
||||
value="<?= $ip_address ?>" maxlength="45" required>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="small mt-1" id="networkIpFeedback"></div>
|
||||
<small class="text-secondary"><?= $network_name ?> — <span class="font-monospace"><?= $network ?></span></small>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +84,12 @@ ob_start();
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// This row's own id is passed so saving an unchanged address doesn't report
|
||||
// itself as a duplicate
|
||||
itflowWatchNetworkIp(<?= $network_id ?>, <?= $ip_id ?>);
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -77,8 +77,23 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
// The count above reflects the search box - the sidebar wants the real total
|
||||
$ip_total = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(ip_id) FROM network_ips WHERE ip_network_id = $network_id"))[0]);
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="client_overview.php?client_id=<?= $client_id ?>"><?= $client_name ?></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="networks.php?<?= $client_url ?>">Networks</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">
|
||||
<i class="fas fa-fw fa-network-wired"></i> <?= $network_name ?>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-3">
|
||||
@@ -95,30 +110,38 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div><i class="fa fa-fw fa-network-wired text-secondary me-2"></i><span class="font-monospace"><?= $network ?></span></div>
|
||||
|
||||
<div class="text-secondary small text-uppercase">Network</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-network-wired text-secondary me-2"></i><?= $network ?></div>
|
||||
|
||||
<?php if ($network_vlan) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-layer-group text-secondary me-2"></i>VLAN <span class="font-monospace"><?= $network_vlan ?></span></div>
|
||||
<div class="text-secondary small text-uppercase">VLAN</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-layer-group text-secondary me-2"></i><?= $network_vlan ?></div>
|
||||
<?php }
|
||||
if ($network_gateway) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-route text-secondary me-2"></i><span class="font-monospace"><?= $network_gateway ?></span></div>
|
||||
<div class="text-secondary small text-uppercase">Gateway</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-route text-secondary me-2"></i><?= $network_gateway ?></div>
|
||||
<?php }
|
||||
if ($network_dhcp_range) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-arrows-alt-h text-secondary me-2"></i><span class="font-monospace"><?= $network_dhcp_range ?></span></div>
|
||||
<div class="text-secondary small text-uppercase">Assignable IP Range</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-arrows-alt-h text-secondary me-2"></i><?= $network_dhcp_range ?></div>
|
||||
<?php }
|
||||
if ($network_primary_dns) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-globe text-secondary me-2"></i><span class="font-monospace"><?= $network_primary_dns ?></span></div>
|
||||
<div class="text-secondary small text-uppercase">Primary DNS</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-globe text-secondary me-2"></i><?= $network_primary_dns ?></div>
|
||||
<?php }
|
||||
if ($network_secondary_dns) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-globe text-secondary me-2"></i><span class="font-monospace"><?= $network_secondary_dns ?></span></div>
|
||||
<div class="text-secondary small text-uppercase">Secondary DNS</div>
|
||||
<div class="font-monospace mb-3"><i class="fa fa-fw fa-globe text-secondary me-2"></i><?= $network_secondary_dns ?></div>
|
||||
<?php }
|
||||
if ($location_name) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-map-marker-alt text-secondary me-2"></i><?= $location_name ?></div>
|
||||
<div class="text-secondary small text-uppercase">Location</div>
|
||||
<div class="mb-3"><i class="fa fa-fw fa-map-marker-alt text-secondary me-2"></i><?= $location_name ?></div>
|
||||
<?php } ?>
|
||||
<div class="mt-3">
|
||||
<a class="btn btn-light btn-sm" href="networks.php?<?= $client_url ?>">
|
||||
<i class="fa fa-fw fa-arrow-left me-2"></i>All Networks
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="text-secondary small text-uppercase">Documented IPs</div>
|
||||
<div class="font-monospace"><i class="fa fa-fw fa-map-pin text-secondary me-2"></i><?= $ip_total ?></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -175,33 +175,127 @@ function isIpInSubnet($ip, $subnet) {
|
||||
}
|
||||
|
||||
/*
|
||||
* The two safeguards on a documented IP, in one place so add, edit and CSV
|
||||
* import can't drift apart: the address must be valid and inside its network's
|
||||
* subnet, and the network must not already have it.
|
||||
* The part of an address a subnet fixes for every host in it, as a display
|
||||
* string with its trailing dot - 192.168.1.0/24 gives "192.168.1.".
|
||||
*
|
||||
* $ip is rewritten by reference to its canonical form, so callers store what
|
||||
* was checked rather than what was typed.
|
||||
* This is what the add/edit form prepends so only the host part is typeable.
|
||||
* Whole octets only, so it tracks the prefix: a /16 fixes two, a /26 fixes
|
||||
* three (the last octet is still typed, and the range check catches a host
|
||||
* outside the block).
|
||||
*
|
||||
* Returns an empty string when there is nothing useful to fix - IPv6, a prefix
|
||||
* under /8, a /32 with no host part at all, or a subnet that won't parse. The
|
||||
* form falls back to a plain full-address input in those cases.
|
||||
*/
|
||||
function ipSubnetFixedOctets($subnet) {
|
||||
|
||||
$parsed = parseSubnet($subnet);
|
||||
|
||||
if ($parsed === false || $parsed['bytes'] !== 4) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$fixed = intdiv($parsed['prefix'], 8);
|
||||
|
||||
if ($fixed < 1 || $fixed > 3) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$octets = explode('.', inet_ntop($parsed['network']));
|
||||
|
||||
return implode('.', array_slice($octets, 0, $fixed)) . '.';
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Turns what was typed into the host part of the form into a full address.
|
||||
*
|
||||
* A complete address is passed straight through, so pasting 192.168.1.10 into
|
||||
* the box still works, as does a CSV that carries full addresses.
|
||||
*/
|
||||
function expandIpSuffix($input, $subnet) {
|
||||
|
||||
$input = trim($input);
|
||||
|
||||
if ($input === '') {
|
||||
return $input;
|
||||
}
|
||||
|
||||
// Already complete - typed in full, pasted, or imported
|
||||
if (normalizeIpAddress($input) !== false) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$fixed = ipSubnetFixedOctets($subnet);
|
||||
|
||||
if ($fixed === '') {
|
||||
return $input;
|
||||
}
|
||||
|
||||
// The host part has to supply exactly the octets the prefix doesn't
|
||||
if (count(explode('.', $input)) !== 4 - substr_count($fixed, '.')) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
return $fixed . $input;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* The reverse, for the edit form: the host part of a stored address, or the
|
||||
* whole address when the subnet fixes nothing.
|
||||
*/
|
||||
function ipSuffixForDisplay($ip, $subnet) {
|
||||
|
||||
$fixed = ipSubnetFixedOctets($subnet);
|
||||
|
||||
// The trailing dot matters - it stops 192.168.1. matching 192.168.10.5
|
||||
if ($fixed === '' || strpos($ip, $fixed) !== 0) {
|
||||
return $ip;
|
||||
}
|
||||
|
||||
return substr($ip, strlen($fixed));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* The two safeguards on a documented IP, in one place so add, edit, CSV import
|
||||
* and the live check behind the form can't drift apart: the address must be
|
||||
* valid and inside its network's subnet, and the network must not already have
|
||||
* it.
|
||||
*
|
||||
* $ip is rewritten by reference to its canonical, fully-expanded form, so
|
||||
* callers store what was checked rather than what was typed.
|
||||
*
|
||||
* Returns an error message for the user, or an empty string when it's good.
|
||||
* Pass the row's own id as $ignore_ip_id when editing so a row doesn't collide
|
||||
* with itself.
|
||||
*
|
||||
* The message is built for flashAlert(), which escapes at render - anything
|
||||
* putting it somewhere else has to run it through alertMessageHtml() first.
|
||||
*/
|
||||
function checkIpForNetwork(&$ip, $network_id, $ignore_ip_id = 0) {
|
||||
|
||||
global $mysqli;
|
||||
|
||||
$network_id = intval($network_id);
|
||||
$subnet = getFieldById('networks', $network_id, 'network');
|
||||
|
||||
$original = trim($ip);
|
||||
$normalized = normalizeIpAddress($original);
|
||||
|
||||
// Host part only from the form, full address from a paste or a CSV
|
||||
$expanded = expandIpSuffix($original, $subnet);
|
||||
|
||||
$normalized = normalizeIpAddress($expanded);
|
||||
|
||||
if ($normalized === false) {
|
||||
return "<strong>$original</strong> is not a valid IP address";
|
||||
// Report the expanded attempt, not the raw input - in a /24 the form
|
||||
// only takes the host part, so "abc" reads better as 192.168.1.abc
|
||||
return "<strong>$expanded</strong> is not a valid IP address";
|
||||
}
|
||||
|
||||
$ip = $normalized;
|
||||
|
||||
$network_id = intval($network_id);
|
||||
$subnet = getFieldById('networks', $network_id, 'network');
|
||||
|
||||
if (!isIpInSubnet($ip, $subnet)) {
|
||||
return "<strong>$ip</strong> is outside <strong>$subnet</strong>";
|
||||
}
|
||||
|
||||
80
js/app.js
80
js/app.js
@@ -883,3 +883,83 @@ function itflowToast(message, type) {
|
||||
delay: 5000
|
||||
}).show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Live IP check for the network IP add/edit modals (agent/modals/network/).
|
||||
*
|
||||
* Calls the same checkIpForNetwork() the POST handler uses, via
|
||||
* ajax.php?network_ip_check, so the field reports exactly the verdict the save
|
||||
* will reach. Advisory only - the handler re-checks on submit, and this never
|
||||
* blocks submission.
|
||||
*
|
||||
* Lives here rather than in the modal because includes/modal_footer.php
|
||||
* re-executes this file on every ajax modal open, so the function is already
|
||||
* defined by the time the modal's inline call runs.
|
||||
*/
|
||||
function itflowWatchNetworkIp(networkId, ipId) {
|
||||
|
||||
var input = document.getElementById('networkIpAddress');
|
||||
var feedback = document.getElementById('networkIpFeedback');
|
||||
|
||||
if (!input || !feedback) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
var lastChecked = null;
|
||||
|
||||
function render(cls, html) {
|
||||
feedback.className = 'small mt-1 ' + cls;
|
||||
feedback.innerHTML = html;
|
||||
}
|
||||
|
||||
function check() {
|
||||
|
||||
var value = input.value.trim();
|
||||
|
||||
if (value === '') {
|
||||
render('', '');
|
||||
input.classList.remove('is-valid', 'is-invalid');
|
||||
lastChecked = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === lastChecked) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastChecked = value;
|
||||
|
||||
itflowGet(
|
||||
'ajax.php',
|
||||
{network_ip_check: 'true', network_id: networkId, ip_id: ipId, ip: value},
|
||||
function (data) {
|
||||
|
||||
var result;
|
||||
|
||||
try {
|
||||
result = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A slow earlier response must not paint over a newer one
|
||||
if (input.value.trim() !== lastChecked) {
|
||||
return;
|
||||
}
|
||||
|
||||
render(result.ok ? 'text-success' : 'text-danger', result.message);
|
||||
input.classList.toggle('is-valid', result.ok);
|
||||
input.classList.toggle('is-invalid', !result.ok);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(check, 350);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', check);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user