Add input validation to SpiriLink

This commit is contained in:
Youssof 2024-10-28 13:42:25 +00:00
parent 7a804b8ba6
commit f74c3f6e1e
3 changed files with 226 additions and 147 deletions

View File

@ -32,26 +32,26 @@ enabled = true;
// Runs the initPage when the document is loaded // Runs the initPage when the document is loaded
document.onload = InitPage(); document.onload = InitPage();
// Save file button // Save file button
document.getElementById("save").addEventListener("click", SaveSettings); document.getElementById("save").addEventListener("click", saveSettings);
// This attempts to read the conf file, if it exists, then it will parse it and fill out the table // This attempts to read the conf file, if it exists, then it will parse it and fill out the table
// if it fails then the values are loaded with defaults. // if it fails then the values are loaded with defaults.
function InitPage() { function InitPage() {
cockpit.script(scriptLocation + "cockpitScript.sh -v") cockpit.script(scriptLocation + "cockpitScript.sh -v")
.then((content) => version.innerHTML=content) .then((content) => version.innerHTML=content)
.catch(error => Fail(error)); .catch(error => displayFail(error));
cockpit.script(scriptLocation + "cockpitScript.sh -u") cockpit.script(scriptLocation + "cockpitScript.sh -u")
.then(function(content) { .then(function(content) {
ipsubnet1.innerHTML=content; ipsubnet1.innerHTML=content;
ipsubnet2.innerHTML=content; ipsubnet2.innerHTML=content;
}) })
.catch(error => Fail(error)); .catch(error => displayFail(error));
file_location.innerHTML = confLocation + "main.conf"; file_location.innerHTML = confLocation + "main.conf";
cockpit.file(confLocation + "main.conf") cockpit.file(confLocation + "main.conf")
.read().then((content, tag) => SuccessReadFile(content)) .read().then((content, tag) => SuccessReadFile(content))
.catch(error => FailureReadFile(error)); .catch(error => failureReadFile(error));
} }
function getValueByKey(text, sectionName, sectionLength, key) { function getValueByKey(text, sectionName, sectionLength, key) {
@ -158,7 +158,7 @@ function SuccessReadFile(content) {
var currentbaudRate = getValueByKey(content, "[UartEndpoint alpha]", 2, "Baud"); var currentbaudRate = getValueByKey(content, "[UartEndpoint alpha]", 2, "Baud");
cockpit.script(scriptLocation + "cockpitScript.sh -s") cockpit.script(scriptLocation + "cockpitScript.sh -s")
.then((content) => AddDropDown(fmuDevice, content.split("\n"), currentfmuDevice)) .then((content) => AddDropDown(fmuDevice, content.split("\n"), currentfmuDevice))
.catch(error => Fail(error)); .catch(error => displayFail(error));
AddDropDown(baudrate, baudRateArray, currentbaudRate); AddDropDown(baudrate, baudRateArray, currentbaudRate);
//UDP Telemetry //UDP Telemetry
@ -225,7 +225,7 @@ function SuccessReadFile(content) {
} }
} }
catch(e) { catch(e) {
FailureReadFile(e); failureReadFile(e);
} }
} }
@ -249,11 +249,11 @@ function AddDropDown(box, theArray, defaultValue) {
} }
} }
catch(e) { catch(e) {
Fail(e) displayFail(e)
} }
} }
function FailureReadFile(error) { function failureReadFile(error) {
// Display error message // Display error message
console.log("Error : " + error.message); console.log("Error : " + error.message);
output.innerHTML = "Error : " + error.message; output.innerHTML = "Error : " + error.message;
@ -267,7 +267,7 @@ function FailureReadFile(error) {
} }
function SaveSettings() { function saveSettings() {
//lets do some validation //lets do some validation
@ -306,7 +306,7 @@ function SaveSettings() {
//open the file for writing, and callback function for modification //open the file for writing, and callback function for modification
cockpit.file(confLocation + "main.conf") cockpit.file(confLocation + "main.conf")
.read().then((content, tag) => SuccessReadforSaveFile(content)) .read().then((content, tag) => SuccessReadforSaveFile(content))
.catch(error => FailureReadFile(error)); .catch(error => failureReadFile(error));
} }
function SuccessReadforSaveFile(content) { function SuccessReadforSaveFile(content) {
@ -335,23 +335,23 @@ function SuccessReadforSaveFile(content) {
content = setValueByKey(content, "[UartEndpoint alpha]",2, "Baud", baudrate.value ) content = setValueByKey(content, "[UartEndpoint alpha]",2, "Baud", baudrate.value )
cockpit.file(confLocation + "main.conf", { superuser : "try" }).replace(content) cockpit.file(confLocation + "main.conf", { superuser : "try" }).replace(content)
.then(Success) .then(displaySuccess)
.catch(Fail); .catch(displayFail);
cockpit.spawn(["systemctl", "restart", "mavlink-router"], { superuser : "try" }); cockpit.spawn(["systemctl", "restart", "mavlink-router"], { superuser : "try" });
} }
catch(e) { catch(e) {
FailureReadFile(e); failureReadFile(e);
} }
} }
function Success() { function displaySuccess() {
result.style.color = "green"; result.style.color = "green";
result.innerHTML = "Success, mavlink-router restarting..."; result.innerHTML = "Success, mavlink-router restarting...";
setTimeout(() => result.innerHTML = "", 5000); setTimeout(() => result.innerHTML = "", 5000);
} }
function Fail(error) { function displayFail(error) {
result.style.color = "red"; result.style.color = "red";
result.innerHTML = error.message; result.innerHTML = error.message;
} }

View File

@ -5,7 +5,7 @@
<title>SpiriLink Configuration</title> <title>SpiriLink Configuration</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="../base1/bootstrap.min.css" type="text/css" rel="stylesheet"> <link href="../base1/bootstrap.min.css" type="text/css" rel="stylesheet">
<link href="../base1/style.css" type="text/css" rel="stylesheet"> <!-- <link href="../base1/style.css" type="text/css" rel="stylesheet"> -->
<script src="../base1/cockpit.js"></script> <script src="../base1/cockpit.js"></script>
<script src="../base1/jquery-3.7.1.min.js"></script> <script src="../base1/jquery-3.7.1.min.js"></script>
</head> </head>
@ -20,163 +20,177 @@
</div> </div>
</div> </div>
<!-- Save Button --> <form class="row" id="spiriLinkForm" action="javascript:void(0);" novalidate>
<div class="row mt-4 mb-4"> <!-- Save Button -->
<div class="col-6"> <div class="row mt-2 mb-3">
<button class="btn btn-primary" id="save">Save/Update</button> <div class="col-6">
<button class="btn btn-primary" id="save">Save/Update</button>
</div>
<div class="col-6 d-flex justify-content-end">
<button id="applyEncryptionFile" class="btn btn-primary">Apply Encryption File from Base Station</button>
</div>
</div> </div>
<div class="col-6 d-flex justify-content-end">
<button id="applyEncryptionFile" class="btn btn-primary">Apply Encryption File from Base Station</button>
</div>
</div>
<pre id="result" class="mt-3 mb-3"></pre> <pre id="result" class="ms-1"></pre>
<!-- WiFi Configuration --> <!-- WiFi Configuration -->
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<img src="assets/icons/settings_wifi.svg" class="me-2"/><span>WiFi Configuration</span> <img src="assets/icons/settings_wifi.svg" class="me-2"/><span>WiFi Configuration</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4"> <div class="col-md-4">
<label for="wifiChannel" class="form-label">WiFi Channel</label> <label for="wifiChannel" class="form-label">WiFi Channel</label>
<select id="wifiChannel" class="form-select"></select> <select id="wifiChannel" class="form-select"></select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="wifiRegion" class="form-label">WiFi Region</label> <label for="wifiRegion" class="form-label">WiFi Region</label>
<select id="wifiRegion" class="form-select"> <select id="wifiRegion" class="form-select">
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="wifiTxPower" class="form-label">WiFi Tx Power (dBm * 100)</label> <label for="wifiTxPower" class="form-label">WiFi Tx Power (dBm * 100)</label>
<input type="number" id="wifiTxPower" class="form-control" <input type="number" id="wifiTxPower" class="form-control"
placeholder="e.g., 3000 for 30dBm"> placeholder="e.g., 3000 for 30dBm">
<div class="invalid-feedback">Must be an integer.</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Temperature Configuration --> <!-- Temperature Configuration -->
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<img src="assets/icons/settings_temp.svg" class="me-2"/><span>Temperature Configuration</span> <img src="assets/icons/settings_temp.svg" class="me-2"/><span>Temperature Configuration</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<label for="tempInterval" class="form-label">Measurement Interval (s)</label> <label for="tempInterval" class="form-label">Measurement Interval (s)</label>
<input type="number" id="tempInterval" class="form-control" value="10"> <input type="number" id="tempInterval" class="form-control" required>
</div> <div class="invalid-feedback">Must be an integer.</div>
<div class="col-md-6"> </div>
<label for="tempWarning" class="form-label">Overheat Warning (°C)</label> <div class="col-md-6">
<input type="number" id="tempWarning" class="form-control" value="60"> <label for="tempWarning" class="form-label">Overheat Warning (°C)</label>
<input type="number" id="tempWarning" class="form-control" step="0.1" required>
<div class="invalid-feedback">Must be a number with at most one decimal. </div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Drone Video Configuration --> <!-- Drone Video Configuration -->
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<img src="assets/icons/settings_video.svg" class="me-2"/><span>Drone Video Configuration</span> <img src="assets/icons/settings_video.svg" class="me-2"/><span>Drone Video Configuration</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="droneVideoFwmark" class="form-label">FWMark</label> <label for="droneVideoFwmark" class="form-label">FWMark</label>
<input type="number" id="droneVideoFwmark" class="form-control"> <input type="number" id="droneVideoFwmark" class="form-control" required>
</div> <div class="invalid-feedback">Must be an integer.</div>
<div class="col-md-3"> </div>
<label for="droneVideoType" class="form-label">Connection Type</label> <div class="col-md-3">
<select id="droneVideoType" class="form-select"></select> <label for="droneVideoType" class="form-label">Connection Type</label>
</div> <select id="droneVideoType" class="form-select" required></select>
<div class="col-md-3"> </div>
<label for="droneVideoIP" class="form-label">IP Address</label> <div class="col-md-3">
<input type="text" id="droneVideoIP" class="form-control"> <label for="droneVideoIP" class="form-label">IP Address</label>
</div> <input type="text" id="droneVideoIP" class="form-control" required>
<div class="col-md-3"> <div class="invalid-feedback">IP Address must be in the format xxx.xxx.xxx.xxx</div>
<label for="droneVideoPort" class="form-label">Port</label> </div>
<input type="number" id="droneVideoPort" class="form-control"> <div class="col-md-3">
<label for="droneVideoPort" class="form-label">Port</label>
<input type="number" id="droneVideoPort" class="form-control" required>
<div class="invalid-feedback">Port must be between 1 and 65535</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Drone MAVLink Configuration --> <!-- Drone MAVLink Configuration -->
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<img src="assets/icons/lan.svg" class="me-2"/><span>Drone MAVLink Configuration</span> <img src="assets/icons/lan.svg" class="me-2"/><span>Drone MAVLink Configuration</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="droneMavlinkFwmark" class="form-label">FWMark</label> <label for="droneMavlinkFwmark" class="form-label">FWMark</label>
<input type="number" id="droneMavlinkFwmark" class="form-control"> <input type="number" id="droneMavlinkFwmark" class="form-control" required>
</div> <div class="invalid-feedback">Must be an integer.</div>
<div class="col-md-3"> </div>
<label for="droneMavlinkType" class="form-label">Connection Type</label> <div class="col-md-3">
<select id="droneMavlinkType" class="form-select"></select> <label for="droneMavlinkType" class="form-label">Connection Type</label>
</div> <select id="droneMavlinkType" class="form-select" required></select>
<div class="col-md-3"> </div>
<label for="droneMavlinkIP" class="form-label">IP Address</label> <div class="col-md-3">
<input type="text" id="droneMavlinkIP" class="form-control"> <label for="droneMavlinkIP" class="form-label">IP Address</label>
</div> <input type="text" id="droneMavlinkIP" class="form-control" required>
<div class="col-md-3"> <div class="invalid-feedback">IP Address must be in the format xxx.xxx.xxx.xxx</div>
<label for="droneMavlinkPort" class="form-label">Port</label> </div>
<input type="number" id="droneMavlinkPort" class="form-control"> <div class="col-md-3">
<label for="droneMavlinkPort" class="form-label">Port</label>
<input type="number" id="droneMavlinkPort" class="form-control" required>
<div class="invalid-feedback">Port must be between 1 and 65535</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- MAVLink Configuration --> <!-- MAVLink Configuration -->
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<img src="assets/icons/settings_ethernet.svg" class="me-2"/><span>MAVLink Settings</span> <img src="assets/icons/settings_ethernet.svg" class="me-2"/><span>MAVLink Settings</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="injectRssi" class="form-label">Inject RSSI</label> <label for="injectRssi" class="form-label">Inject RSSI</label>
<input type="checkbox" id="injectRssi" class="form-check-input"> <input type="checkbox" id="injectRssi" class="form-check-input">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="mavlinkSysId" class="form-label">System ID</label> <label for="mavlinkSysId" class="form-label">System ID</label>
<input type="number" id="mavlinkSysId" class="form-control" required> <input type="number" id="mavlinkSysId" class="form-control" min="1" max="255" required>
</div> <div class="invalid-feedback">System ID must be between 1 and 255</div>
<div class="col-md-3"> </div>
<label for="mavlinkCompId" class="form-label">Component ID</label> <div class="col-md-3">
<input type="number" id="mavlinkCompId" class="form-control"> <label for="mavlinkCompId" class="form-label">Component ID</label>
</div> <input type="number" id="mavlinkCompId" class="form-control" min="1" max="255" required>
<div class="col-md-3"> <div class="invalid-feedback">Comp ID must be between 1 and 255</div>
<label for="mavlinkTcpPort" class="form-label">MAVLink TCP Port</label> </div>
<input type="number" id="mavlinkTcpPort" class="form-control" <div class="col-md-3">
placeholder="None if left blank"> <label for="mavlinkTcpPort" class="form-label">MAVLink TCP Port</label>
<input type="number" id="mavlinkTcpPort" class="form-control"
placeholder="None if left blank">
<div class="invalid-feedback">Port must be between 1 and 65535</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </form>
</div> </div>
<script src="spirilink.js"></script> <script src="spirilink.js"></script>

View File

@ -23,6 +23,7 @@ const mavlinkSysId = document.getElementById("mavlinkSysId");
const mavlinkCompId = document.getElementById("mavlinkCompId"); const mavlinkCompId = document.getElementById("mavlinkCompId");
const mavlinkTcpPort = document.getElementById("mavlinkTcpPort"); const mavlinkTcpPort = document.getElementById("mavlinkTcpPort");
const spiriLinkForm = document.getElementById("spiriLinkForm");
const output = document.getElementById("output"); const output = document.getElementById("output");
const wifiChannels = [ const wifiChannels = [
@ -74,7 +75,7 @@ function initPage() {
const inputs = document.querySelectorAll("input, select, textarea"); const inputs = document.querySelectorAll("input, select, textarea");
inputs.forEach(input => { inputs.forEach(input => {
input.addEventListener("focus", clearResult); input.addEventListener("focus", resetForm);
}); });
cockpit.file(confLocation) cockpit.file(confLocation)
@ -85,6 +86,7 @@ function initPage() {
// Load configuration values from the configuration file // Load configuration values from the configuration file
function successReadFile(content) { function successReadFile(content) {
try { try {
// WiFi Configuration // WiFi Configuration
// Remove surrounding quotes from wifi_region value if present // Remove surrounding quotes from wifi_region value if present
const currentWifiChannel = getValueByKey(content, "common", "wifi_channel"); const currentWifiChannel = getValueByKey(content, "common", "wifi_channel");
@ -131,14 +133,69 @@ function successReadFile(content) {
} }
} }
// Log error message and display it
// TODO: Add more error handling for failed file reads
function failureReadFile(error) { function failureReadFile(error) {
// Display error message console.error("Error : " + error.message);
console.log("Error : " + error.message);
displayFail(error.message) displayFail(error.message)
} }
// Validate the form using non-standard input validation conditions.
// Additional checks for IP, port, and system/component ID fields
// Returns true if all fields are valid, false otherwise
function validateSpiriLinkForm(form) {
const ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const portformat = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
let isValid = true;
defaultValidation = form.checkValidity();
const inputs = form.querySelectorAll("input, select, textarea");
inputs.forEach(input => {
if (input.value === "") {
isValid = !input.hasAttribute("required");
setValidationVisuals(input, isValid);
} else {
if (input.id === "droneMavlinkIP" || input.id === "droneVideoIP") {
isValid = input.value.match(ipformat);
setValidationVisuals(input, isValid);
}
else if (input.id === "droneMavlinkPort" || input.id === "droneVideoPort" || input.id === "mavlinkTcpPort") {
isValid = input.value.match(portformat);
setValidationVisuals(input, isValid);
}
else if (input.id === "mavlinkSysId" || input.id === "mavlinkCompId") {
isValid = input.value > 1 || input.value < 255;
setValidationVisuals(input, isValid);
}
}
});
form.classList.add("was-validated");
if (!isValid || !defaultValidation) return false;
return true;
}
// Set validation visuals for input fields
// Adds or removes is-invalid class based on the validity of the input
// The setCustomValidity function triggers the actual field visuals.
// Giving it an empty string assumes the input is valid.
function setValidationVisuals(input, isValid) {
if (isValid) {
input.classList.remove("is-invalid");
input.setCustomValidity("");
} else {
input.classList.add("is-invalid");
input.setCustomValidity("Invalid input");
}
}
// Save configuration values to the configuration file // Save configuration values to the configuration file
function saveSettings() { function saveSettings() {
const dataIsValid = validateSpiriLinkForm(spiriLinkForm);
if (!dataIsValid) return;
try { try {
cockpit.file(confLocation) cockpit.file(confLocation)
.read() .read()
@ -231,9 +288,18 @@ function displayFail(error) {
result.innerHTML = "Error : " + error; result.innerHTML = "Error : " + error;
} }
// Clear the result message // Reset & clear form data
function clearResult() { function resetForm() {
result.innerHTML = ""; result.innerHTML = "";
const inputs = spiriLinkForm.querySelectorAll("input, select, textarea");
inputs.forEach(input => {
input.setCustomValidity("");
input.classList.remove("is-invalid");
input.classList.remove("is-valid");
});
spiriLinkForm.classList.remove("was-validated");
spiriLinkForm.classList.add("needs-validation");
} }
function addDropDown(box, pairs, defaultValue) { function addDropDown(box, pairs, defaultValue) {
@ -245,7 +311,6 @@ function addDropDown(box, pairs, defaultValue) {
option.text = pairs[i][1]; option.text = pairs[i][1];
box.add(option); box.add(option);
if (defaultValue === option.value) { if (defaultValue === option.value) {
console.log("found")
box.value = option.value; box.value = option.value;
} }
} }