main
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* D-Star reflector link/unlink page — service status grid + a form
|
||||||
|
* that posts back to itself to issue
|
||||||
|
* `sudo remotecontrold "<module>" link never "<reflector>"`
|
||||||
|
* (or `unlink`) against the running ircDDBGateway.
|
||||||
|
*
|
||||||
|
* Hit directly via /admin/admin.php (the form action self-posts);
|
||||||
|
* not inline-included by any current parent page. The historical
|
||||||
|
* "Loaded inline by /admin/index.php" docstring claim was stale —
|
||||||
|
* there is no `include 'admin.php'` site in the codebase today.
|
||||||
|
* Reachable only by direct navigation; admin/configure.php
|
||||||
|
* configures the *password* for the same `remotecontrold` daemon
|
||||||
|
* but doesn't issue link/unlink commands.
|
||||||
|
*
|
||||||
|
* Service-status row pgreps for the six daemons (DStarRepeater,
|
||||||
|
* MMDVMHost, ircDDBgateway, timeserver, pistar-watchdog,
|
||||||
|
* pistar-keeper) and renders a green/red dot per service.
|
||||||
|
*
|
||||||
|
* Input validation: preg_match whitelist on Module / RefName /
|
||||||
|
* Letter / Link verb (uppercase A-Z and digits only). That guards
|
||||||
|
* against shell-quote escape but does not substitute for CSRF —
|
||||||
|
* a forged POST with valid-looking values could otherwise trigger
|
||||||
|
* link/unlink under the operator's basic-auth session. csrf_verify()
|
||||||
|
* below closes that gap before any sudo runs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/csrf.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships and csrf_field() at line ~98 has a token to
|
||||||
|
// emit) and rejects forged POSTs cleanly with 403 before the
|
||||||
|
// `sudo remotecontrold "<module>" link|unlink ...` state-change
|
||||||
|
// in the POST handler runs.
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<b>Service Status</b>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><a class=tooltip href="#">DStarRepeater<span><b>DStarRepeater</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">MMDVMHost<span><b>DStarRepeater</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">ircDDBgateway<span><b>ircDDBgateway</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">timeserver<span><b>timeserver</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">pistar-watchdog<span><b>pistar-watchdog</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">pistar-keeper<span><b>pistar-keeper</b></span></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php exec ("pgrep dstarrepeaterd", $dstarrepeaterpid); if (!empty($dstarrepeaterpid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
<td><?php exec ("pgrep MMDVMHost", $mmdvmhostpid); if (!empty($mmdvmhostpid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
<td><?php exec ("pgrep ircddbgatewayd", $ircddbgatewaypid); if (!empty($ircddbgatewaypid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
<td><?php exec ("pgrep timeserverd", $timeserverpid); if (!empty($timeserverpid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
<td><?php exec ("pgrep -f -a /usr/local/sbin/pistar-watchdog | sed '/pgrep/d'", $watchdogpid); if (!empty($watchdogpid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
<td><?php exec ("pgrep -f -a /usr/local/sbin/pistar-keeper | sed '/pgrep/d'", $keeperpid); if (!empty($keeperpid)) { echo "<img src=\"images/20green.png\">"; } else { echo "<img src=\"images/20red.png\">"; } ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<?php if (!empty($_POST)):
|
||||||
|
// Each $_POST read uses the `?? ''` null-coalesce so that:
|
||||||
|
// - a missing key (Link / RefName / Letter / Module never sent) and
|
||||||
|
// - a key unset by the preg_match sanitiser below (e.g. Link
|
||||||
|
// contained non-uppercase chars and was unset)
|
||||||
|
// both resolve to '' instead of triggering PHP 8's "Undefined array
|
||||||
|
// key" E_WARNING. Behaviour is unchanged: '' fails every "X" == "Y"
|
||||||
|
// comparison the way an undefined key did under PHP 7's E_NOTICE.
|
||||||
|
// `(... ?? '')` is parenthesised because == binds tighter than ??.
|
||||||
|
if (preg_match('/[^A-Z]/', $_POST["Link"] ?? '')) { unset ($_POST["Link"]);}
|
||||||
|
if (($_POST["Link"] ?? '') == "LINK") {
|
||||||
|
if (preg_match('/[^A-Z0-9]/', $_POST["RefName"] ?? '')) { unset ($_POST["RefName"]);}
|
||||||
|
if (preg_match('/[^A-Z]/', $_POST["Letter"] ?? '')) { unset ($_POST["Letter"]);}
|
||||||
|
if (preg_match('/[^A-Z0-9 ]/', $_POST["Module"] ?? '')) { unset ($_POST["Module"]);}
|
||||||
|
}
|
||||||
|
if (($_POST["Link"] ?? '') == "UNLINK") {
|
||||||
|
if (preg_match('/[^A-Z0-9 ]/', $_POST["Module"] ?? '')) { unset ($_POST["Module"]);}
|
||||||
|
}
|
||||||
|
if (empty($_POST["RefName"]) || empty($_POST["Letter"]) || empty($_POST["Module"])) { echo "Somthing wrong with your input, try again";}
|
||||||
|
|
||||||
|
|
||||||
|
else {
|
||||||
|
$targetRef = $_POST["RefName"]." ".$_POST["Letter"];
|
||||||
|
$module = $_POST["Module"];
|
||||||
|
|
||||||
|
if (strlen($module) != 8) { //Fix the length of the module information
|
||||||
|
$moduleFixedCs= strlen($module) - 1; //Length of the string, -1
|
||||||
|
$moduleFixedBand = substr($module, -1); //Single Band Letter in the 8th position
|
||||||
|
$moduleFixedCallPad = str_pad(substr($module, 0, $moduleFixedCs), 7); //Pad the callsign area to 7 chars
|
||||||
|
$module = $moduleFixedCallPad.$moduleFixedBand; //Re add the band information
|
||||||
|
};
|
||||||
|
|
||||||
|
$unlinkCommand = "sudo remotecontrold \"".$module."\" unlink";
|
||||||
|
$linkCommand = "sudo remotecontrold \"".$module."\" link never \"".$targetRef."\"";
|
||||||
|
|
||||||
|
if (($_POST["Link"] ?? '') == "LINK") {
|
||||||
|
echo "<b>Reflector Connector</b>\n";
|
||||||
|
echo "<table>\n<tr><th><a class=tooltip href=\"#\">Command Output<span><b>Command Output</b></span></th></tr>\n<tr><td>";
|
||||||
|
// remotecontrold output is text from the daemon; escape on
|
||||||
|
// display defence-in-depth (the input fields above were
|
||||||
|
// already preg_match-whitelist-validated).
|
||||||
|
echo htmlspecialchars((string)exec($linkCommand), ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "</tr></td>\n</table>\n";
|
||||||
|
}
|
||||||
|
if (($_POST["Link"] ?? '') == "UNLINK") {
|
||||||
|
echo "<b>Reflector Connector</b>\n";
|
||||||
|
echo "<table>\n<tr><th><a class=tooltip href=\"#\">Command Output<span><b>Command Output</b></span></th></tr>\n<tr><td>";
|
||||||
|
echo htmlspecialchars((string)exec($unlinkCommand), ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "</tr></td>\n</table>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($_POST);
|
||||||
|
echo '<script type="text/javascript">setTimeout(function() { window.location=window.location;},2000);</script>';
|
||||||
|
|
||||||
|
else: ?>
|
||||||
|
<b>Reflector Connector</b>
|
||||||
|
<form action="//<?php echo htmlentities($_SERVER['HTTP_HOST']).htmlentities($_SERVER['PHP_SELF']); ?>" method="post">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th id="lblModule" width="150"><a class=tooltip href="#">Radio Module<span><b>Radio Module</b></span></th>
|
||||||
|
<th id="lblReflector" width="180"><a class=tooltip href="#">Reflector<span><b>Reflector</b></span></th>
|
||||||
|
<th id="lblLinkUnlink" width="150"><a class=tooltip href="#">Link / Un-Link<span><b>Link / Un-Link</b></span></th>
|
||||||
|
<th><a class=tooltip href="#">Action<span><b>Action</b></span></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<select aria-labelledby="lblModule" name="Module">
|
||||||
|
<?php for($i = 1;$i < 5; $i++){
|
||||||
|
$param="repeaterBand" . $i;
|
||||||
|
if((isset($configs[$param])) && strlen($configs[$param]) == 1) {
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
$module = $configs[$param];
|
||||||
|
$rcall = sprintf("%-7.7s%-1.1s",$MYCALL,$module);
|
||||||
|
$param="repeaterCall" . $i;
|
||||||
|
if(isset($configs[$param])) { $rptrcall=sprintf("%-7.7s%-1.1s",$configs[$param],$module); } else { $rptrcall = $rcall;}
|
||||||
|
print "<option>$rptrcall</option>\n";
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td role="group" aria-labelledby="lblReflector">
|
||||||
|
<select aria-label="Number" name="RefName">
|
||||||
|
<?php
|
||||||
|
$dcsFile = fopen("/usr/local/etc/DCS_Hosts.txt", "r");
|
||||||
|
$dplusFile = fopen("/usr/local/etc/DPlus_Hosts.txt", "r");
|
||||||
|
$dextraFile = fopen("/usr/local/etc/DExtra_Hosts.txt", "r");
|
||||||
|
|
||||||
|
while (!feof($dcsFile)) {
|
||||||
|
$dcsLine = fgets($dcsFile);
|
||||||
|
if (strpos($dcsLine, 'DCS') !== FALSE && strpos($dcsLine, '#') === FALSE)
|
||||||
|
echo " <option>".substr($dcsLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
fclose($dcsFile);
|
||||||
|
|
||||||
|
echo " <option selected>REF001</option>\n";
|
||||||
|
|
||||||
|
while (!feof($dplusFile)) {
|
||||||
|
$dplusLine = fgets($dplusFile);
|
||||||
|
if (strpos($dplusLine, 'REF') !== FALSE && strpos($dplusLine, '#') === FALSE && strpos($dplusLine, 'REF001') === FALSE)
|
||||||
|
echo " <option>".substr($dplusLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
fclose($dplusFile);
|
||||||
|
|
||||||
|
while (!feof($dextraFile)) {
|
||||||
|
$dextraLine = fgets($dextraFile);
|
||||||
|
if (strpos($dextraLine, 'XRF') !== FALSE && strpos($dextraLine, '#') === FALSE)
|
||||||
|
echo " <option>".substr($dextraLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
fclose($dextraFile);
|
||||||
|
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<select aria-label="Module" name="Letter">
|
||||||
|
<option>A</option>
|
||||||
|
<option>B</option>
|
||||||
|
<option selected>C</option>
|
||||||
|
<option>D</option>
|
||||||
|
<option>E</option>
|
||||||
|
<option>F</option>
|
||||||
|
<option>G</option>
|
||||||
|
<option>H</option>
|
||||||
|
<option>I</option>
|
||||||
|
<option>J</option>
|
||||||
|
<option>K</option>
|
||||||
|
<option>L</option>
|
||||||
|
<option>M</option>
|
||||||
|
<option>N</option>
|
||||||
|
<option>O</option>
|
||||||
|
<option>P</option>
|
||||||
|
<option>Q</option>
|
||||||
|
<option>R</option>
|
||||||
|
<option>S</option>
|
||||||
|
<option>T</option>
|
||||||
|
<option>U</option>
|
||||||
|
<option>V</option>
|
||||||
|
<option>W</option>
|
||||||
|
<option>X</option>
|
||||||
|
<option>Y</option>
|
||||||
|
<option>Z</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td role="radiogroup" aria-labelledby="lblLinkUnlink">
|
||||||
|
<input id="rbLink" type="radio" name="Link" value="LINK" checked><label for="rbLink">Link</label>
|
||||||
|
<input id="rbUnlink" type="radio" name="Link" value="UNLINK"><label for="rbUnlink">UnLink</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="submit" value="Request Change">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
exec ("pgrep pistar-keeper", $pids);
|
||||||
|
if (!empty($pids))
|
||||||
|
{
|
||||||
|
echo "<br />\n";
|
||||||
|
echo "<b>PiStar-Keeper Logbook</b><input type=button onClick=\"location.href='/admin/pistar-keeper-download.php'\" value=\"Download Logbook\">\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo " <tr>\n";
|
||||||
|
echo " <th><a class=tooltip href=\"#\">PiStar-Keeper Log Entries (UTC)<span><b>PiStar-Keeper Log Entries (UTC)</b></span></th>\n";
|
||||||
|
echo " </tr>\n";
|
||||||
|
|
||||||
|
exec ("tail -n 5 /var/pistar-keeper/pistar-keeper.log", $lines);
|
||||||
|
$counter = 0;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
echo "<tr><td align=\"left\">".$lines[$counter]."</td></tr>\n";
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "</table>\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<?php
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/calibration.php") {
|
||||||
|
|
||||||
|
if (isset($_GET['action'])) {
|
||||||
|
if ($_GET['action'] === 'start') {
|
||||||
|
system('sudo fuser -k 33273/udp > /dev/null 2>&1');
|
||||||
|
system('nc -ulp 33273 | sudo -i script -qfc "/usr/local/sbin/pistar-mmdvmcal" /tmp/pi-star_mmdvmcal.log > /dev/null 2>&1 &');
|
||||||
|
}
|
||||||
|
else if (($_GET['action'] === 'saveoffset')) {
|
||||||
|
if (isset($_GET['param']) && strlen($_GET['param'])) {
|
||||||
|
system('sudo mount -o remount,rw /');
|
||||||
|
system('sudo sed -i "/RXOffset=/c\\RXOffset='.intval($_GET['param']).'" /etc/mmdvmhost');
|
||||||
|
system('sudo sed -i "/TXOffset=/c\\TXOffset='.intval($_GET['param']).'" /etc/mmdvmhost');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['cmd']) && strlen($_GET['cmd'])) {
|
||||||
|
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||||
|
socket_bind($sock, '127.0.0.1', 33272) || exit();
|
||||||
|
socket_sendto($sock, $_GET['cmd'], strlen($_GET['cmd']), 0, '127.0.0.1', 33273);
|
||||||
|
if (isset($_GET['param']) && strlen($_GET['param'])) {
|
||||||
|
usleep(500*1000);
|
||||||
|
socket_sendto($sock, $_GET['param']."\n", strlen($_GET['param'])+1, 0, '127.0.0.1', 33273);
|
||||||
|
}
|
||||||
|
if ($_GET['cmd'] === 'q') {
|
||||||
|
sleep(1);
|
||||||
|
socket_sendto($sock, "\n", 1, 0, '127.0.0.1', 33273); //send something to kill the pipe, also \n may be useful if something went wrong and mmdvmcal is waiting some param input
|
||||||
|
}
|
||||||
|
socket_close($sock);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
//unset($_SESSION['mmdvmcal_offset']);
|
||||||
|
if (file_exists('/tmp/pi-star_mmdvmcal.log')) {
|
||||||
|
$_SESSION['mmdvmcal_offset'] = filesize('/tmp/pi-star_mmdvmcal.log');
|
||||||
|
} else {
|
||||||
|
$_SESSION['mmdvmcal_offset'] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
//session_start();
|
||||||
|
if (!file_exists('/tmp/pi-star_mmdvmcal.log')) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen('/tmp/pi-star_mmdvmcal.log', 'rb');
|
||||||
|
if (isset($_SESSION['mmdvmcal_offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['mmdvmcal_offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['mmdvmcal_offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['mmdvmcal_offset']);
|
||||||
|
$_SESSION['mmdvmcal_offset'] += strlen($data);
|
||||||
|
echo nl2br($data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['mmdvmcal_offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$RXFrequency = intval(exec('grep "RXFrequency" /etc/mmdvmhost | awk -F "=" \'{print $2}\''));
|
||||||
|
$RXOffset = intval(exec('grep "RXOffset" /etc/mmdvmhost | awk -F "=" \'{print $2}\''));
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="Chinese" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="CDN 校准" />
|
||||||
|
<meta name="KeyWords" content="CDN" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>CDN - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - 校准";?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/plotly-basic.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var rxoffset = ~~'<?php echo $RXOffset; ?>';
|
||||||
|
|
||||||
|
function sendaction(action='', param='') {
|
||||||
|
if (action === 'start') { document.getElementById("btnStart").disabled = true; }
|
||||||
|
if (action === 'saveoffset') { rxoffset = ~~param }
|
||||||
|
$.ajax({
|
||||||
|
url: 'calibration.php',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
'action': action,
|
||||||
|
'param': param
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
success: function(msg) {}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendcmd_lock=false;
|
||||||
|
|
||||||
|
function sendcmd(cmd='', param='') {
|
||||||
|
if (sendcmd_lock) { return false; }
|
||||||
|
if (param !== '') { sendcmd_lock = true; } //if we have param, lock to prevent cmd overlap while waiting param
|
||||||
|
$.ajax({
|
||||||
|
url: 'calibration.php',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
'cmd': cmd,
|
||||||
|
'param': param
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
success: function(msg) {},
|
||||||
|
complete: function() { sendcmd_lock = false; }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cnt=0; tcnt=0;
|
||||||
|
var cfrms=0; cbits=0, cberr=0;
|
||||||
|
var tfrms=0; tbits=0, tberr=0;
|
||||||
|
var eot=false;
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/calibration.php?ajax', function(data) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
<?php if (isset($_GET['debug'])) { ?>
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
if (("\n"+data).includes("Version:")) {
|
||||||
|
setTimeout(function(){ sendcmd('e', (~~'<?php echo $RXFrequency; ?>'+rxoffset).toString() ); }, 1000);
|
||||||
|
}
|
||||||
|
if (("\n"+data).includes("Finnished...")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20red.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20red.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20red.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20red.png');
|
||||||
|
document.getElementById("btnStart").disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (("\n"+data).includes("\nBER Test Mode (FEC) for D-Star")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20red.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20red.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20red.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20red.png');
|
||||||
|
}
|
||||||
|
if (("\n"+data).includes("\nBER Test Mode (FEC) for DMR Simplex")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20green.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20red.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20red.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20red.png');
|
||||||
|
}
|
||||||
|
if (("\n"+data).includes("\nBER Test Mode (FEC) for YSF")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20red.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20green.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20red.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20red.png');
|
||||||
|
}
|
||||||
|
if (("\n"+data).includes("\nBER Test Mode (FEC) for P25")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20red.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20red.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20green.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20red.png');
|
||||||
|
}
|
||||||
|
if (("\n"+data).includes("\nBER Test Mode (FEC) for NXDN")) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$('#ledDStar').attr("src", 'images/20red.png');
|
||||||
|
$('#ledDMR').attr("src", 'images/20red.png');
|
||||||
|
$('#ledYSF').attr("src", 'images/20red.png');
|
||||||
|
$('#ledP25').attr("src", 'images/20red.png');
|
||||||
|
$('#ledNXDN').attr("src", 'images/20green.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.includes("voice end received,")) {
|
||||||
|
eot=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex = / frequency: (\d+)/g
|
||||||
|
while (match = regex.exec(data)) {
|
||||||
|
$('#ledStart').attr("src", 'images/20green.png');
|
||||||
|
$("#lblFrequency").text(match[1] + ' Hz');
|
||||||
|
$("#lblOffset").text(~~match[1] - ~~'<?php echo $RXFrequency; ?>');
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex = /\% \((\d+)\/(\d+)\)/g
|
||||||
|
while (match = regex.exec(data)) {
|
||||||
|
cfrms += 1;
|
||||||
|
cberr += ~~match[1];
|
||||||
|
cbits += ~~match[2];
|
||||||
|
tfrms += 1;
|
||||||
|
tberr += ~~match[1];
|
||||||
|
tbits += ~~match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cbits > 0) {
|
||||||
|
cnt++; tcnt++;
|
||||||
|
var updfrq = $('#sltUpdFrq').val();
|
||||||
|
if ((tcnt % updfrq == 0) || eot) {
|
||||||
|
//$('#tail').append(cfrms +' , '+ cberr +' / '+ cbits +' , '+ (cberr/cbits*100).toFixed(2) + '%<br>');
|
||||||
|
$("#lblFrames").text(cfrms);
|
||||||
|
$("#lblBits").text(cbits);
|
||||||
|
$("#lblErrors").text(cberr);
|
||||||
|
$("#lblBER").text((cberr/cbits*100).toFixed(2)+'%');
|
||||||
|
Plotly.extendTraces('chart', { x:[[cnt]], y:[[cberr/cbits*100]] }, [0]);
|
||||||
|
if(cnt > 60*3) {
|
||||||
|
Plotly.relayout('chart', {
|
||||||
|
xaxis: {range: [cnt-60*3,cnt]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cfrms=0; cbits=0; cberr=0;
|
||||||
|
|
||||||
|
//$('#tail').append('total: ' + tfrms +' , '+ tberr +' / '+ tbits +' , '+ (tberr/tbits*100).toFixed(2) + '%<br>');
|
||||||
|
$("#lblTFrames").text(tfrms);
|
||||||
|
$("#lblTBits").text(tbits);
|
||||||
|
$("#lblTErrors").text(tberr);
|
||||||
|
$("#lblTBER").text((tberr/tbits*100).toFixed(2)+'%');
|
||||||
|
$("#lblTSec").text(tcnt);
|
||||||
|
if (eot) {
|
||||||
|
eot=false;
|
||||||
|
tfrms=0; tbits=0; tberr=0; tcnt=0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">CDN:<?php echo $configPistarRelease['Pi-Star']['Version']?> / Dashboard:<?php echo $version; ?></div>
|
||||||
|
<h1>CDN - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - 校准";?></h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/power.php" style="color: #ffffff;"><?php echo $lang['power'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<h2>校准工具</h2>
|
||||||
|
|
||||||
|
<div class="cal-grid">
|
||||||
|
<div class="settings-card cal-panel">
|
||||||
|
<h3>运行</h3>
|
||||||
|
<div class="cal-btn-row"><input name="btnStart" type="button" id="btnStart" onclick="sendaction('start');" value="开始" /><img src="images/20red.png" name="ledStart" width="20" height="20" id="ledStart" /></div>
|
||||||
|
<div class="cal-btn-row"><input name="btnStop" type="button" id="btnStop" onclick="sendcmd('q');" value="停止" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card cal-panel">
|
||||||
|
<h3>模式</h3>
|
||||||
|
<div class="cal-btn-row"><input name="btnDStar" type="button" id="btnDStar" onclick="sendcmd('k');" value="D-Star" /><img src="images/20red.png" name="ledDStar" width="20" height="20" id="ledDStar" /></div>
|
||||||
|
<div class="cal-btn-row"><input name="btnDMR" type="button" id="btnDMR" onclick="sendcmd('b');" value="DMR" /><img src="images/20red.png" name="ledDMR" width="20" height="20" id="ledDMR" /></div>
|
||||||
|
<div class="cal-btn-row"><input name="btnYSF" type="button" id="btnYSF" onclick="sendcmd('J');" value="YSF" /><img src="images/20red.png" name="ledYSF" width="20" height="20" id="ledYSF" /></div>
|
||||||
|
<div class="cal-btn-row"><input name="btnP25" type="button" id="btnP25" onclick="sendcmd('j');" value="P25" /><img src="images/20red.png" name="ledP25" width="20" height="20" id="ledP25" /></div>
|
||||||
|
<div class="cal-btn-row"><input name="btnNXDN" type="button" id="btnNXDN" onclick="sendcmd('n');" value="NXDN" /><img src="images/20red.png" name="ledNXDN" width="20" height="20" id="ledNXDN" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card cal-panel">
|
||||||
|
<h3>频率</h3>
|
||||||
|
<div class="field-row"><div>基准频率</div><div id="lblBaseFreq"><?php echo $RXFrequency; ?> Hz</div></div>
|
||||||
|
<div class="field-row"><div>频率</div><div id="lblFrequency"><?php echo $RXFrequency + $RXOffset; ?> Hz</div></div>
|
||||||
|
<div class="field-row"><div>偏移量</div><div>
|
||||||
|
<input name="btnFreqM" type="button" id="btnFreqM" onclick="sendcmd('f');" value="-" />
|
||||||
|
<span id="lblOffset" style="display:inline-block;min-width:5ch;text-align:center;"><?php echo $RXOffset; ?></span>
|
||||||
|
<input name="btnFreqP" type="button" id="btnFreqP" onclick="sendcmd('F');" value="+" />
|
||||||
|
</div></div>
|
||||||
|
<div class="field-row"><div>步进</div><div><input type="button" onclick="sendcmd('z','25');" value="25" /> <input type="button" onclick="sendcmd('z','50');" value="50" /> <input type="button" onclick="sendcmd('z','100');" value="100" /></div></div>
|
||||||
|
<div class="field-row"><div></div><div><input name="button8" type="button" onclick="sendaction('saveoffset',$('#lblOffset').text());" value="保存偏移量" /></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card cal-panel">
|
||||||
|
<h3>实时统计</h3>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>当前</th>
|
||||||
|
<th>总计</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left">帧数:</td>
|
||||||
|
<td id="lblFrames"> </td>
|
||||||
|
<td id="lblTFrames"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left">比特数:</td>
|
||||||
|
<td id="lblBits"> </td>
|
||||||
|
<td id="lblTBits"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left">错误数:</td>
|
||||||
|
<td id="lblErrors"> </td>
|
||||||
|
<td id="lblTErrors"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left">误码率:</td>
|
||||||
|
<td id="lblBER"> </td>
|
||||||
|
<td id="lblTBER"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left">秒数:</td>
|
||||||
|
<td id="lblSec" style="padding:0;"><select name="sltUpdFrq" id="sltUpdFrq" style="margin:0;">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="5" selected="selected">5</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="30">30</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td id="lblTSec"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<div id="chart"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
Plotly.newPlot('chart', [{
|
||||||
|
x: [0],
|
||||||
|
y: [0],
|
||||||
|
type: 'scatter',
|
||||||
|
mode: 'lines',
|
||||||
|
fill: 'tozeroy',
|
||||||
|
line: {color: '#4f46e5'}
|
||||||
|
}], {title:'误码率 (BER)', xaxis:{title:'秒',rangemode:'tozero'}, yaxis:{title:'%',rangemode:'tozero',range:[0,5]} }, {staticPlot: true});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($_GET['debug'])) { ?>
|
||||||
|
<div class="settings-card"><div id="tail"></div></div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
// Look up a callsign's registered city/province/country from the NXDN.csv
|
||||||
|
// radio ID database (radiowo.com format: RADIO_ID,CALLSIGN,NAME,CITY,STATE,COUNTRY,REMARKS)
|
||||||
|
// so configure.php can auto-fill the City/Country fields.
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$callsign = isset($_GET['callsign']) ? strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $_GET['callsign'])) : '';
|
||||||
|
$result = array('found' => false);
|
||||||
|
|
||||||
|
$csvFile = '/usr/local/etc/NXDN.csv';
|
||||||
|
if ($callsign !== '' && is_readable($csvFile)) {
|
||||||
|
if (($fh = fopen($csvFile, 'r')) !== false) {
|
||||||
|
fgetcsv($fh); // skip header row
|
||||||
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
|
if (isset($row[1]) && strtoupper($row[1]) === $callsign) {
|
||||||
|
$result['found'] = true;
|
||||||
|
$result['city'] = isset($row[3]) ? $row[3] : '';
|
||||||
|
$result['state'] = isset($row[4]) ? $row[4] : '';
|
||||||
|
$result['country'] = isset($row[5]) ? $row[5] : '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($result);
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Forced password-change destination — Layer 2 of the default-password
|
||||||
|
* protection (see config/banner_warnings.inc for the redirect logic).
|
||||||
|
*
|
||||||
|
* Reached only when the operator is connecting from a remote subnet
|
||||||
|
* AND still using the factory default basic-auth password. Renders a
|
||||||
|
* single-purpose form: change the password, nothing else.
|
||||||
|
*
|
||||||
|
* The htpasswd / chpasswd update flow is a verbatim mirror of the
|
||||||
|
* password-change section in admin/configure.php (~line 440 onward).
|
||||||
|
* Keep the two in sync — any future fix to one belongs in the other.
|
||||||
|
*
|
||||||
|
* On success: redirect to /admin/. The Layer 2 trigger will no longer
|
||||||
|
* fire (password is no longer 'raspberry'), so the operator returns
|
||||||
|
* to normal dashboard navigation.
|
||||||
|
*
|
||||||
|
* Direct-URL access by an operator who is NOT using the default
|
||||||
|
* password just redirects to /admin/ — the page is only useful when
|
||||||
|
* the trigger condition holds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF: rejects forged POSTs cleanly with 403 before any chpasswd /
|
||||||
|
// htpasswd side-effect. Bootstraps the session on GET so the token
|
||||||
|
// cookie ships with the form render.
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// If the operator's password is NOT the default, they don't need
|
||||||
|
// this page. Send them to the main admin dashboard.
|
||||||
|
//
|
||||||
|
// Done BEFORE any HTML output so the redirect is clean.
|
||||||
|
if (!_pistar_default_password_in_use()) {
|
||||||
|
header('Location: /admin/', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once('../config/language.php');
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
require_once('../config/version.php');
|
||||||
|
|
||||||
|
// Mirror of admin/configure.php password change (~line 440). Keep
|
||||||
|
// these two paths byte-equivalent so a fix to one always lands in
|
||||||
|
// the other. proc_open() + stdin pipes — password never on argv,
|
||||||
|
// never reaches a shell. chpasswd FIRST so PAM rejection blocks
|
||||||
|
// the htpasswd write and keeps web/shell auth in sync.
|
||||||
|
//
|
||||||
|
// Two-field verification: the form posts both `adminPassword` and
|
||||||
|
// `adminPasswordConfirm`. Client-side functions.js compares them on
|
||||||
|
// keystroke and only enables the submit button when they match;
|
||||||
|
// the server-side check below is defence in depth — never trust
|
||||||
|
// the client to enforce verification.
|
||||||
|
$passwordChanged = false;
|
||||||
|
$passwordRejected = false;
|
||||||
|
$rejectReason = '';
|
||||||
|
if (!empty($_POST['adminPassword'])) {
|
||||||
|
$rawPassword = stripslashes(trim($_POST['adminPassword']));
|
||||||
|
$rawPasswordConfirm = isset($_POST['adminPasswordConfirm'])
|
||||||
|
? stripslashes(trim($_POST['adminPasswordConfirm']))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$hasIllegalChar = preg_match('/[\x00\r\n]/', $rawPassword);
|
||||||
|
$tooLong = strlen($rawPassword) > 256;
|
||||||
|
$isStillDefault = hash_equals('raspberry', $rawPassword);
|
||||||
|
// hash_equals on user-supplied vs user-supplied isn't a security
|
||||||
|
// boundary (no secret on either side), but it's the right idiom
|
||||||
|
// for password compares — and guards against any future caller
|
||||||
|
// mistaking the result for an authentication signal.
|
||||||
|
$confirmMatches = hash_equals($rawPassword, $rawPasswordConfirm);
|
||||||
|
|
||||||
|
if ($rawPassword === '' || $hasIllegalChar || $tooLong) {
|
||||||
|
$passwordRejected = true;
|
||||||
|
$rejectReason = 'invalid';
|
||||||
|
error_log('Pi-Star change_password_required.php: adminPassword rejected '
|
||||||
|
. '(empty, contains NUL/CR/LF, or > 256 bytes)');
|
||||||
|
} elseif ($isStillDefault) {
|
||||||
|
$passwordRejected = true;
|
||||||
|
$rejectReason = 'default';
|
||||||
|
error_log('Pi-Star change_password_required.php: adminPassword rejected (still the default)');
|
||||||
|
} elseif (!$confirmMatches) {
|
||||||
|
$passwordRejected = true;
|
||||||
|
$rejectReason = 'mismatch';
|
||||||
|
error_log('Pi-Star change_password_required.php: adminPassword rejected '
|
||||||
|
. '(confirmation does not match)');
|
||||||
|
} else {
|
||||||
|
// Pi-Star runs with `/` mounted read-only by default. chpasswd
|
||||||
|
// and htpasswd both write to files on the rootfs (/etc/shadow
|
||||||
|
// and /var/www/.htpasswd respectively); without a remount-rw
|
||||||
|
// they fail with PAM "Authentication token manipulation error"
|
||||||
|
// and the symptom looks like every password is rejected. The
|
||||||
|
// matching configure.php POST handler does this at line ~348.
|
||||||
|
// Re-seal the rootfs at the bottom of this block on every
|
||||||
|
// exit path (success, PAM rejection, htpasswd failure).
|
||||||
|
system('sudo mount -o remount,rw /');
|
||||||
|
|
||||||
|
$descriptors = array(
|
||||||
|
0 => array('pipe', 'r'),
|
||||||
|
1 => array('pipe', 'w'),
|
||||||
|
2 => array('pipe', 'w'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Linux user via chpasswd. Hardcoded command; payload on stdin only.
|
||||||
|
$cpExit = 1;
|
||||||
|
$cpStderr = '';
|
||||||
|
$proc = proc_open('sudo -n /usr/sbin/chpasswd', $descriptors, $pipes);
|
||||||
|
if (is_resource($proc)) {
|
||||||
|
fwrite($pipes[0], 'pi-star:' . $rawPassword . "\n");
|
||||||
|
fclose($pipes[0]);
|
||||||
|
stream_get_contents($pipes[1]); fclose($pipes[1]);
|
||||||
|
$cpStderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
||||||
|
$cpExit = proc_close($proc);
|
||||||
|
} else {
|
||||||
|
error_log('Pi-Star change_password_required.php: failed to spawn chpasswd');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cpExit !== 0) {
|
||||||
|
// PAM rejection (libpwquality, history, length, etc.).
|
||||||
|
$passwordRejected = true;
|
||||||
|
$rejectReason = 'pam';
|
||||||
|
error_log('Pi-Star change_password_required.php: chpasswd exit='
|
||||||
|
. $cpExit . '; stderr=' . trim($cpStderr));
|
||||||
|
} else {
|
||||||
|
// 2. Apache basic-auth file. /var/www/.htpasswd is owned by
|
||||||
|
// www-data, so no sudo needed.
|
||||||
|
$htExit = 1;
|
||||||
|
$htStderr = '';
|
||||||
|
$proc = proc_open('htpasswd -i /var/www/.htpasswd pi-star',
|
||||||
|
$descriptors, $pipes);
|
||||||
|
if (is_resource($proc)) {
|
||||||
|
fwrite($pipes[0], $rawPassword);
|
||||||
|
fclose($pipes[0]);
|
||||||
|
stream_get_contents($pipes[1]); fclose($pipes[1]);
|
||||||
|
$htStderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
||||||
|
$htExit = proc_close($proc);
|
||||||
|
if ($htExit !== 0) {
|
||||||
|
error_log('Pi-Star change_password_required.php: htpasswd exit='
|
||||||
|
. $htExit . ' (Linux password updated); stderr='
|
||||||
|
. trim($htStderr));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log('Pi-Star change_password_required.php: failed to spawn htpasswd');
|
||||||
|
}
|
||||||
|
// Tighten /var/www/.htpasswd from htpasswd's default 644
|
||||||
|
// to 600. Owner stays www-data so nginx (running as
|
||||||
|
// www-data) keeps reading for basic-auth checks; other
|
||||||
|
// local users no longer see the bcrypt hash.
|
||||||
|
//
|
||||||
|
// Surface chmod failure into the error log. The realistic
|
||||||
|
// failure modes are (a) htpasswd spawn failed and the
|
||||||
|
// file is in some unexpected state, or (b) a future
|
||||||
|
// refactor moves this call outside the rootfs-rw window
|
||||||
|
// (it sits inside one today — see commit aa999d49).
|
||||||
|
// Either way, the file silently staying mode 644 would
|
||||||
|
// be worth knowing about.
|
||||||
|
if (@chmod('/var/www/.htpasswd', 0600) === false) {
|
||||||
|
error_log('Pi-Star change_password_required.php: chmod 0600 /var/www/.htpasswd failed (file may stay mode 644 until next save)');
|
||||||
|
}
|
||||||
|
// chpasswd succeeded; treat as success even if htpasswd
|
||||||
|
// failed (operator can retry — same behaviour as
|
||||||
|
// configure.php). The browser will re-prompt for the new
|
||||||
|
// credentials on the next request.
|
||||||
|
$passwordChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-seal the rootfs on every exit path out of the writable
|
||||||
|
// block. sync first so the shadow / htpasswd writes land on
|
||||||
|
// disk before the remount makes the FS read-only again.
|
||||||
|
system('sudo sync && sudo sync && sudo sync && sudo mount -o remount,ro /');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star — Default password change required" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/functions.js"></script>
|
||||||
|
<title>Pi-Star — Change Default Password</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Pi-Star — Default Password Change Required</h1>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php if ($passwordChanged) { ?>
|
||||||
|
<table>
|
||||||
|
<tr><th>Working...</th></tr>
|
||||||
|
<tr><td>Password changed. Your browser will re-prompt for the new credentials shortly.</td></tr>
|
||||||
|
</table>
|
||||||
|
<script type="text/javascript">setTimeout(function() { window.location='/admin/'; }, 5000);</script>
|
||||||
|
<?php } else { ?>
|
||||||
|
<p>You are connecting from outside the local network and your dashboard is still using the
|
||||||
|
factory default password. Set a new password to continue.</p>
|
||||||
|
<?php if ($passwordRejected) {
|
||||||
|
// Distinct message per rejection class so the operator knows
|
||||||
|
// exactly what to fix without having to guess.
|
||||||
|
switch ($rejectReason) {
|
||||||
|
case 'mismatch':
|
||||||
|
$msg = '<b>Passwords do not match.</b> Please enter the same value in both fields.';
|
||||||
|
break;
|
||||||
|
case 'default':
|
||||||
|
$msg = '<b>Password rejected.</b> The new password must be different from the factory default.';
|
||||||
|
break;
|
||||||
|
case 'pam':
|
||||||
|
$msg = '<b>Password rejected by the system.</b> It must satisfy the system password-quality rules (length, complexity, history).';
|
||||||
|
break;
|
||||||
|
case 'invalid':
|
||||||
|
default:
|
||||||
|
$msg = '<b>Password rejected.</b> It must not contain NUL / CR / LF bytes and must be at most 256 bytes long.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<p style="color: #f01010;"><?php echo $msg; ?></p>
|
||||||
|
<?php } ?>
|
||||||
|
<form id="adminPassForm" action="/admin/change_password_required.php" method="post" autocomplete="off">
|
||||||
|
<?php echo csrf_field_html(); ?>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="right" width="40%"><label for="pass1">New password:</label></td>
|
||||||
|
<td align="left">
|
||||||
|
<input type="password" name="adminPassword" id="pass1"
|
||||||
|
size="32" autocomplete="new-password"
|
||||||
|
onkeyup="checkPass(); return false;" required="required" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right"><label for="pass2">Confirm password:</label></td>
|
||||||
|
<td align="left">
|
||||||
|
<input type="password" name="adminPasswordConfirm" id="pass2"
|
||||||
|
size="32" autocomplete="new-password"
|
||||||
|
onkeyup="checkPass(); return false;" required="required" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="center">
|
||||||
|
<!-- id="submitpwd" matches the existing checkPass() in
|
||||||
|
functions.js which enables this button only when both
|
||||||
|
fields match. Server still re-verifies, so the JS gate
|
||||||
|
is UX, not security. -->
|
||||||
|
<input type="submit" id="submitpwd" value="Change Password" disabled="disabled" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date('Y'); ?>.<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../config/
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Configuration backup & restore.
|
||||||
|
*
|
||||||
|
* Two POST actions:
|
||||||
|
* - download: zips the operator's config files into
|
||||||
|
* /tmp/config_backup.zip and sends it as a download. Files
|
||||||
|
* are listed once in {@see backup_files()} and shared with
|
||||||
|
* the restore handler so the two paths cannot drift.
|
||||||
|
* - restore: accepts a ZIP upload, validates it, extracts a
|
||||||
|
* known-safe subset into /tmp/config_restore/, stops every
|
||||||
|
* DV service, atomically installs each file at its mapped
|
||||||
|
* target, then restarts services.
|
||||||
|
*
|
||||||
|
* Security model (post-#16 remediation):
|
||||||
|
* - Upload is finfo_file()-typechecked, size-capped, then
|
||||||
|
* moved to a fixed temp filename — the operator-supplied
|
||||||
|
* name never reaches a path concat.
|
||||||
|
* - ZIP entries are filtered against backup_files() before
|
||||||
|
* extraction. Anything not on the allowlist (including any
|
||||||
|
* entry whose name contains `/`, `\`, or starts with `.`)
|
||||||
|
* is silently skipped — no zip-slip vector remains.
|
||||||
|
* - The blind `sudo mv -v -f /tmp/config_restore/* /etc/`
|
||||||
|
* pattern is replaced by per-file `sudo install -m … -o
|
||||||
|
* root -g root` driven by the same allowlist. No glob ever
|
||||||
|
* reaches the shell after extraction.
|
||||||
|
* - The two post-restore "re-apply from restored config"
|
||||||
|
* paths (timezone via shell-interpolated config.php grep,
|
||||||
|
* and remotePassword via shell-interpolated sed-i) are
|
||||||
|
* replaced by data-side propagation: timezone validated
|
||||||
|
* against DateTimeZone::listIdentifiers() and passed via
|
||||||
|
* escapeshellarg(), remotePassword routed through
|
||||||
|
* config_writer's privileged-flat helper (no shell sees
|
||||||
|
* the value).
|
||||||
|
*
|
||||||
|
* Backup contents (and therefore the restore allowlist) are
|
||||||
|
* defined ONCE in {@see backup_files()}. Files NOT in that list
|
||||||
|
* are deliberately NOT backed up — notably /etc/hostapd/hostapd.conf
|
||||||
|
* and /root/.Remote Control, which carry secrets that should
|
||||||
|
* never leave the device in a portable form.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/config_writer.php');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical map of files in a Pi-Star backup ZIP.
|
||||||
|
*
|
||||||
|
* Single source of truth: the backup loop iterates this map to
|
||||||
|
* decide what to package, and the restore loop iterates this map
|
||||||
|
* to decide what to install (and where). ZIP entries with names
|
||||||
|
* outside the keys here are silently ignored on restore.
|
||||||
|
*
|
||||||
|
* key = basename inside the ZIP (the backup is a zip -j
|
||||||
|
* "junk paths" archive — every entry sits at the
|
||||||
|
* archive root with no directory component).
|
||||||
|
* value = absolute target path on the device.
|
||||||
|
*
|
||||||
|
* Files NOT in this map are deliberately excluded from backup:
|
||||||
|
* - /etc/hostapd/hostapd.conf (AP wpa_passphrase secret)
|
||||||
|
* - /root/.Remote Control (linker password secret)
|
||||||
|
* - /etc/sudoers* / /etc/passwd (system auth)
|
||||||
|
*
|
||||||
|
* @return array<string, array{0:string, 1:int, 2:string, 3:string}>
|
||||||
|
* basename => [path, mode, owner, group]
|
||||||
|
*/
|
||||||
|
function backup_files()
|
||||||
|
{
|
||||||
|
// Each entry is [path, mode, owner, group]. The mode/owner/group
|
||||||
|
// tuple is what the restore loop passes to `sudo install -m -o
|
||||||
|
// -g`, and MUST match the corresponding /etc/sudoers.d/pistar-
|
||||||
|
// dashboard PISTAR_DASH_INSTALL line for that destination — or
|
||||||
|
// sudo will reject the install and the file will silently fail
|
||||||
|
// to restore. Pre-2026-05-01 the restore loop used a hardcoded
|
||||||
|
// `install -m 644 -o root -g root` for every entry, which
|
||||||
|
// worked under the old NOPASSWD: ALL sudoers but breaks under
|
||||||
|
// the path-scoped allowlist for: the .key files (require 600
|
||||||
|
// www-data:www-data so the dashboard's per-page sudo-less
|
||||||
|
// parse_ini_file() reads still work — see fulledit_bmapikey.php
|
||||||
|
// for the full rationale), wpa_supplicant.conf (600 root:root),
|
||||||
|
// and the three system files (dhcpcd.conf, hosts, hostname)
|
||||||
|
// which have no install entry in the deployed sudoers at all
|
||||||
|
// and rely on a re-spun image to gain them.
|
||||||
|
return array(
|
||||||
|
// Network / WiFi
|
||||||
|
'dhcpcd.conf' => array('/etc/dhcpcd.conf', 644, 'root', 'root'),
|
||||||
|
'wpa_supplicant.conf' => array('/etc/wpa_supplicant/wpa_supplicant.conf', 600, 'root', 'root'),
|
||||||
|
// Gateway daemon configs (flat key=value INI-ish)
|
||||||
|
'ircddbgateway' => array('/etc/ircddbgateway', 644, 'root', 'root'),
|
||||||
|
'mmdvmhost' => array('/etc/mmdvmhost', 644, 'root', 'root'),
|
||||||
|
'dstarrepeater' => array('/etc/dstarrepeater', 644, 'root', 'root'),
|
||||||
|
'dapnetgateway' => array('/etc/dapnetgateway', 644, 'root', 'root'),
|
||||||
|
'p25gateway' => array('/etc/p25gateway', 644, 'root', 'root'),
|
||||||
|
'm17gateway' => array('/etc/m17gateway', 644, 'root', 'root'),
|
||||||
|
'ysfgateway' => array('/etc/ysfgateway', 644, 'root', 'root'),
|
||||||
|
'ysf2dmr' => array('/etc/ysf2dmr', 644, 'root', 'root'),
|
||||||
|
'dgidgateway' => array('/etc/dgidgateway', 644, 'root', 'root'),
|
||||||
|
'nxdngateway' => array('/etc/nxdngateway', 644, 'root', 'root'),
|
||||||
|
'dmrgateway' => array('/etc/dmrgateway', 644, 'root', 'root'),
|
||||||
|
'mobilegps' => array('/etc/mobilegps', 644, 'root', 'root'),
|
||||||
|
'starnetserver' => array('/etc/starnetserver', 644, 'root', 'root'),
|
||||||
|
'timeserver' => array('/etc/timeserver', 644, 'root', 'root'),
|
||||||
|
// Mode markers (operator has at most one of these)
|
||||||
|
'dstar-radio.mmdvmhost' => array('/etc/dstar-radio.mmdvmhost', 644, 'root', 'root'),
|
||||||
|
'dstar-radio.dstarrepeater' => array('/etc/dstar-radio.dstarrepeater', 644, 'root', 'root'),
|
||||||
|
// Pi-Star service / dashboard config
|
||||||
|
'pistar-remote' => array('/etc/pistar-remote', 644, 'root', 'root'),
|
||||||
|
'hosts' => array('/etc/hosts', 644, 'root', 'root'),
|
||||||
|
'hostname' => array('/etc/hostname', 644, 'root', 'root'),
|
||||||
|
// .key files: 600 www-data so unprivileged dashboard reads
|
||||||
|
// (banner_warnings.inc, bm_links.php, bm_manager.php — all
|
||||||
|
// parse_ini_file() without sudo) still work post-restore.
|
||||||
|
'bmapi.key' => array('/etc/bmapi.key', 600, 'www-data', 'www-data'),
|
||||||
|
'dapnetapi.key' => array('/etc/dapnetapi.key', 600, 'www-data', 'www-data'),
|
||||||
|
'pistar-css.ini' => array('/etc/pistar-css.ini', 644, 'root', 'root'),
|
||||||
|
'RSSI.dat' => array('/usr/local/etc/RSSI.dat', 644, 'root', 'root'),
|
||||||
|
'ircddblocal.php' => array('/var/www/dashboard/config/ircddblocal.php', 644, 'root', 'root'),
|
||||||
|
'config.php' => array('/var/www/dashboard/config/config.php', 644, 'root', 'root'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before the download / restore handlers run.
|
||||||
|
//
|
||||||
|
// CSRF protection here makes C1's zip-slip / restore-pipeline RCE
|
||||||
|
// harder to exploit (attacker can no longer trigger restore via a
|
||||||
|
// cross-site click); the underlying bugs in the restore handler
|
||||||
|
// remain — they are tracked separately as the C1/C2 work-on-hold.
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/config_backup.php") {
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
// session_start() is no longer called here — csrf_verify() at
|
||||||
|
// the top of the file already started the session via
|
||||||
|
// csrf_session_start(). A second session_start() would emit a
|
||||||
|
// "session is already active" NOTICE on PHP 8.x.
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Power" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['backup_restore'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">Pi-Star:<?php echo $configPistarRelease['Pi-Star']['Version']?> / <?php echo $lang['dashboard'].": ".$version; ?></div>
|
||||||
|
<h1>Pi-Star <?php echo $lang['digital_voice']." - ".$lang['backup_restore'];?></h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/power.php" style="color: #ffffff;"><?php echo $lang['power'];?></a> |
|
||||||
|
<a href="/admin/update.php" style="color: #ffffff;"><?php echo $lang['update'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php if (!empty($_POST)) {
|
||||||
|
echo '<table width="100%">'."\n";
|
||||||
|
|
||||||
|
if ( $_POST["action"] === "download" ) {
|
||||||
|
echo "<tr><th colspan=\"2\">".$lang['backup_restore']."</th></tr>\n";
|
||||||
|
|
||||||
|
$output = "Finding config files to be backed up\n";
|
||||||
|
$backupDir = "/tmp/config_backup";
|
||||||
|
$backupZip = "/tmp/config_backup.zip";
|
||||||
|
$hostNameInfo = exec('cat /etc/hostname');
|
||||||
|
|
||||||
|
$output .= shell_exec("sudo rm -rf " . escapeshellarg($backupZip) . " 2>&1");
|
||||||
|
$output .= shell_exec("sudo rm -rf " . escapeshellarg($backupDir) . " 2>&1");
|
||||||
|
$output .= shell_exec("sudo mkdir " . escapeshellarg($backupDir) . " 2>&1");
|
||||||
|
|
||||||
|
// Iterate the canonical backup map so the backup and the
|
||||||
|
// restore allowlist cannot drift from each other. dhcpcd.conf
|
||||||
|
// is special-cased (only included when the operator has a
|
||||||
|
// static IP configured) — everything else is unconditional;
|
||||||
|
// missing source files are silently skipped (the cp returns
|
||||||
|
// an error to stderr that nobody reads, same as the legacy
|
||||||
|
// behaviour).
|
||||||
|
foreach (backup_files() as $basename => $meta) {
|
||||||
|
// $meta is [path, mode, owner, group]. The backup half
|
||||||
|
// only needs the path — mode/owner/group are consumed
|
||||||
|
// on the restore-half install command.
|
||||||
|
$srcpath = $meta[0];
|
||||||
|
if ($basename === 'dhcpcd.conf') {
|
||||||
|
// Only back up dhcpcd.conf if the operator has set a
|
||||||
|
// static IP — otherwise restoring would clobber
|
||||||
|
// working DHCP config on the target device.
|
||||||
|
if (!shell_exec('cat /etc/dhcpcd.conf | grep "static ip_address" | grep -v "#"')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file_exists($srcpath)) {
|
||||||
|
$output .= shell_exec(
|
||||||
|
"sudo cp " . escapeshellarg($srcpath) . " "
|
||||||
|
. escapeshellarg($backupDir . '/' . $basename) . " 2>&1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "Compressing backup files\n";
|
||||||
|
$output .= shell_exec("sudo zip -j " . escapeshellarg($backupZip) . " " . escapeshellarg($backupDir) . "/* 2>&1");
|
||||||
|
$output .= "Starting download\n";
|
||||||
|
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
|
||||||
|
if (file_exists($backupZip)) {
|
||||||
|
$utc_time = gmdate('Y-m-d H:i:s');
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('Y-M-d');
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
if ($hostNameInfo != "pi-star") {
|
||||||
|
header('Content-Disposition: attachment; filename="'.basename("Pi-Star_Config_".$hostNameInfo."_".$local_time.".zip").'"');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
header('Content-Disposition: attachment; filename="'.basename("Pi-Star_Config_$local_time.zip").'"');
|
||||||
|
}
|
||||||
|
header('Content-Transfer-Encoding: binary');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate');
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Content-Length: ' . filesize($backupZip));
|
||||||
|
ob_clean();
|
||||||
|
flush();
|
||||||
|
readfile($backupZip);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
if ( $_POST["action"] === "restore" ) {
|
||||||
|
echo "<tr><th colspan=\"2\">Config Restore</th></tr>\n";
|
||||||
|
$output = "Uploading your Config data\n";
|
||||||
|
|
||||||
|
// Wrap the whole restore in a do/while(false) so an early
|
||||||
|
// bail-out (upload validation failure, ZIP parse error,
|
||||||
|
// unsafe entry name, etc.) can break cleanly out of the
|
||||||
|
// sequence without an `if/else` pyramid. PHP 7.0+ idiom.
|
||||||
|
do {
|
||||||
|
|
||||||
|
// Hardened restore pipeline. See the file-level docblock for
|
||||||
|
// the security model. Constants here:
|
||||||
|
// - $target_dir is hardcoded; the operator-supplied filename
|
||||||
|
// never touches a path concat.
|
||||||
|
// - $upload_path is also hardcoded; we move the uploaded
|
||||||
|
// temp file to a known name before any further processing.
|
||||||
|
// - $max_zip_bytes caps the upload; a real Pi-Star backup
|
||||||
|
// is ~25 KB, nginx already caps the body at 512 KB, so
|
||||||
|
// 256 KB is generous.
|
||||||
|
$target_dir = '/tmp/config_restore';
|
||||||
|
$upload_path = '/tmp/config_restore_upload.zip';
|
||||||
|
$max_zip_bytes = 256 * 1024;
|
||||||
|
$allowlist = backup_files();
|
||||||
|
|
||||||
|
shell_exec("sudo rm -rf " . escapeshellarg($target_dir) . " 2>&1");
|
||||||
|
shell_exec("rm -f " . escapeshellarg($upload_path) . " 2>&1");
|
||||||
|
shell_exec("mkdir -p " . escapeshellarg($target_dir) . " 2>&1");
|
||||||
|
|
||||||
|
// ----- Upload validation -------------------------------------
|
||||||
|
$upload_ok = false;
|
||||||
|
$err_msg = '';
|
||||||
|
if (!isset($_FILES['fileToUpload']) ||
|
||||||
|
$_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$err_msg = 'No file uploaded, or upload failed.';
|
||||||
|
} elseif ($_FILES['fileToUpload']['size'] <= 0 ||
|
||||||
|
$_FILES['fileToUpload']['size'] > $max_zip_bytes) {
|
||||||
|
$err_msg = 'Upload size out of range (expected up to '
|
||||||
|
. round($max_zip_bytes / 1024) . ' KB).';
|
||||||
|
} else {
|
||||||
|
$tmp_name = $_FILES['fileToUpload']['tmp_name'];
|
||||||
|
// finfo_file() reads the magic bytes — much harder for an
|
||||||
|
// attacker to fake than the client-supplied $_FILES['type'].
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$magic = $finfo ? finfo_file($finfo, $tmp_name) : '';
|
||||||
|
if ($finfo) finfo_close($finfo);
|
||||||
|
if ($magic !== 'application/zip') {
|
||||||
|
$err_msg = 'Uploaded file is not a ZIP archive (detected '
|
||||||
|
. htmlspecialchars($magic, ENT_QUOTES, 'UTF-8') . ').';
|
||||||
|
} elseif (!move_uploaded_file($tmp_name, $upload_path)) {
|
||||||
|
$err_msg = 'Could not stage upload to ' . $upload_path . '.';
|
||||||
|
} else {
|
||||||
|
$upload_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$upload_ok) {
|
||||||
|
$output .= $err_msg . "\n";
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
// Don't emit </table> here — the outer code does it once
|
||||||
|
// after the if-action blocks finish. The "Go Back" button
|
||||||
|
// is rendered AFTER the table closes (see end of file).
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- ZIP entry validation ---------------------------------
|
||||||
|
// Open the archive and decide which entries to extract BEFORE
|
||||||
|
// any extraction happens. An entry is admitted iff:
|
||||||
|
// - its name appears verbatim as a key in $allowlist
|
||||||
|
// (basename → target map), AND
|
||||||
|
// - its name contains no `/`, no `\`, and does not start
|
||||||
|
// with `.` (defence in depth — the backup writer uses
|
||||||
|
// `zip -j` so directory components are never legitimate).
|
||||||
|
// Entries outside the allowlist are silently skipped — that's
|
||||||
|
// the documented behaviour for forward-compat with future
|
||||||
|
// Pi-Star versions adding new files.
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($upload_path) !== true) {
|
||||||
|
$output .= "Could not open the uploaded ZIP.\n";
|
||||||
|
@unlink($upload_path);
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$admit = array();
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$name = $zip->getNameIndex($i);
|
||||||
|
if ($name === false || $name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strpos($name, '/') !== false ||
|
||||||
|
strpos($name, '\\') !== false ||
|
||||||
|
strpos($name, "\0") !== false ||
|
||||||
|
$name[0] === '.') {
|
||||||
|
// Path-traversal / hidden / NUL-injection attempt.
|
||||||
|
// Reject the whole archive — this is well outside any
|
||||||
|
// Pi-Star-generated backup shape. break 2 exits the
|
||||||
|
// for() AND the surrounding do/while(false) early-
|
||||||
|
// exit block in one hop.
|
||||||
|
$zip->close();
|
||||||
|
@unlink($upload_path);
|
||||||
|
$output .= "Refusing ZIP — entry '"
|
||||||
|
. htmlspecialchars(substr($name, 0, 64), ENT_QUOTES, 'UTF-8')
|
||||||
|
. "' has an unsafe name.\n";
|
||||||
|
error_log("config_backup: rejected ZIP with unsafe entry '$name'");
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
if (isset($allowlist[$name])) {
|
||||||
|
$admit[$name] = $i;
|
||||||
|
}
|
||||||
|
// else: silently ignored — unknown filename.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract only the admitted entries into $target_dir. We use
|
||||||
|
// ZipArchive::extractTo() with an explicit entry list so
|
||||||
|
// ANYTHING outside that list is left in the archive and
|
||||||
|
// never written to disk.
|
||||||
|
$entries_to_extract = array_keys($admit);
|
||||||
|
if (empty($entries_to_extract)) {
|
||||||
|
$zip->close();
|
||||||
|
@unlink($upload_path);
|
||||||
|
$output .= "ZIP contained no recognised Pi-Star config files.\n";
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!$zip->extractTo($target_dir, $entries_to_extract)) {
|
||||||
|
$zip->close();
|
||||||
|
@unlink($upload_path);
|
||||||
|
$output .= "Failed to extract ZIP entries.\n";
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
@unlink($upload_path);
|
||||||
|
$output .= "Your .zip file was uploaded and unpacked ("
|
||||||
|
. count($entries_to_extract) . " files).\n";
|
||||||
|
|
||||||
|
// ----- Service stop ----------------------------------------
|
||||||
|
$output .= "Stopping Services.\n";
|
||||||
|
$stop_services = array(
|
||||||
|
'cron.service', 'dstarrepeater.service', 'mmdvmhost.service',
|
||||||
|
'ircddbgateway.service', 'timeserver.service',
|
||||||
|
'pistar-watchdog.service', 'pistar-remote.service',
|
||||||
|
'ysfgateway.service', 'ysf2dmr.service', 'p25gateway.service',
|
||||||
|
'nxdngateway.service', 'm17gateway.service',
|
||||||
|
'dapnetgateway.service', 'mobilegps.service',
|
||||||
|
);
|
||||||
|
foreach ($stop_services as $svc) {
|
||||||
|
shell_exec('sudo systemctl stop ' . escapeshellarg($svc) . ' 2>&1');
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_exec('sudo mount -o remount,rw / 2>&1');
|
||||||
|
|
||||||
|
// ----- Per-file install ------------------------------------
|
||||||
|
// Replace the previous blind `sudo mv -v -f /tmp/.../* /etc/`
|
||||||
|
// with a per-file `sudo install -m 644 -o root -g root` driven
|
||||||
|
// by the canonical map. install is atomic on same-fs (rename)
|
||||||
|
// and falls back to safe copy across filesystems. The mode and
|
||||||
|
// owner are forced regardless of how the file arrived in the
|
||||||
|
// archive.
|
||||||
|
$output .= "Writing new Config\n";
|
||||||
|
|
||||||
|
// Tear down stale dstar-radio.* markers first — only one of
|
||||||
|
// the two can be in use, and the restored archive may carry
|
||||||
|
// a different mode than the device currently has. Use
|
||||||
|
// `rm -rf` (not `rm -f`) so the call matches the
|
||||||
|
// PISTAR_DASH_MARKERS sudoers entry — different argv tokens
|
||||||
|
// yield different sudoers matches, and only `-rf` is
|
||||||
|
// allowlisted.
|
||||||
|
shell_exec('sudo rm -rf /etc/dstar-radio.* 2>&1');
|
||||||
|
|
||||||
|
$installed = 0;
|
||||||
|
foreach ($admit as $basename => $_idx) {
|
||||||
|
$src = $target_dir . '/' . $basename;
|
||||||
|
$meta = $allowlist[$basename];
|
||||||
|
$target = $meta[0];
|
||||||
|
$mode = (string)$meta[1];
|
||||||
|
$owner = $meta[2];
|
||||||
|
$group = $meta[3];
|
||||||
|
if (!file_exists($src)) {
|
||||||
|
// Should be impossible after a successful extractTo,
|
||||||
|
// but guard anyway — extraction can fail per-entry on
|
||||||
|
// unusual archives without throwing.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Per-file install args from backup_files() metadata —
|
||||||
|
// mode/owner/group must match the deployed sudoers
|
||||||
|
// PISTAR_DASH_INSTALL line for $target or sudo will
|
||||||
|
// reject. The hardcoded `install -m 644 -o root -g root`
|
||||||
|
// here used to silently fail on .key files (require
|
||||||
|
// 600 www-data:www-data) and wpa_supplicant.conf
|
||||||
|
// (600 root:root) under the path-scoped sudoers.
|
||||||
|
$cmd = 'sudo install -m ' . $mode
|
||||||
|
. ' -o ' . escapeshellarg($owner)
|
||||||
|
. ' -g ' . escapeshellarg($group) . ' '
|
||||||
|
. escapeshellarg($src) . ' '
|
||||||
|
. escapeshellarg($target);
|
||||||
|
$rc = 0; $cmd_out = array();
|
||||||
|
exec($cmd . ' 2>&1', $cmd_out, $rc);
|
||||||
|
if ($rc === 0) {
|
||||||
|
$installed++;
|
||||||
|
} else {
|
||||||
|
$output .= " install failed for $basename: "
|
||||||
|
. implode(' / ', $cmd_out) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output .= "Installed $installed file(s).\n";
|
||||||
|
|
||||||
|
// ----- Post-restore re-applies (data-side, not shell) -----
|
||||||
|
|
||||||
|
// Timezone: the restored config.php contains the operator's
|
||||||
|
// timezone in a date_default_timezone_set('…') call. We want
|
||||||
|
// the OS clock to match the dashboard's view. PRE-fix this
|
||||||
|
// path was a shell pipeline interpolating the grepped value;
|
||||||
|
// post-fix we extract via PHP-side parsing and validate
|
||||||
|
// strictly against PHP's own list of valid timezone IDs
|
||||||
|
// before passing through escapeshellarg().
|
||||||
|
$cfg_path = '/var/www/dashboard/config/config.php';
|
||||||
|
if (is_readable($cfg_path)) {
|
||||||
|
$cfg = file_get_contents($cfg_path);
|
||||||
|
if (preg_match("/date_default_timezone_set\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/",
|
||||||
|
$cfg, $m)) {
|
||||||
|
$tz = $m[1];
|
||||||
|
if (in_array($tz, DateTimeZone::listIdentifiers(), true)) {
|
||||||
|
shell_exec('sudo timedatectl set-timezone '
|
||||||
|
. escapeshellarg($tz) . ' 2>&1');
|
||||||
|
} else {
|
||||||
|
error_log("config_backup: skipping unknown timezone '$tz' from restored config.php");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ircDDBGateway Remote Control password: the restored
|
||||||
|
// /etc/ircddbgateway carries `remotePassword=...`; the
|
||||||
|
// /root/.Remote Control sibling file (mode 600 root:root,
|
||||||
|
// not in the backup set) needs the same value or the
|
||||||
|
// remotecontrold tool can't reach the daemon.
|
||||||
|
//
|
||||||
|
// Pre-fix this was a shell-interpolated `sudo sed -i ...`.
|
||||||
|
// Post-fix: read via PHP-side fopen/fgets (the file is mode
|
||||||
|
// 644 root:root after we just installed it, so www-data can
|
||||||
|
// read it back), validate, then route through the helper's
|
||||||
|
// privileged-flat editor — same primitive the C6.7 fix uses
|
||||||
|
// and also what the configure.php confPassword handler now
|
||||||
|
// uses. No shell ever sees the value.
|
||||||
|
$rp_target = '/etc/ircddbgateway';
|
||||||
|
if (is_readable($rp_target)) {
|
||||||
|
$rp = '';
|
||||||
|
foreach (file($rp_target, FILE_IGNORE_NEW_LINES) as $line) {
|
||||||
|
if (strpos($line, 'remotePassword=') === 0) {
|
||||||
|
$rp = substr($line, strlen('remotePassword='));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ircDDBGateway accepts arbitrary printable bytes for the
|
||||||
|
// password; the helper's NUL/CR/LF guard is the floor.
|
||||||
|
if ($rp !== '' && !preg_match('/[\x00\r\n]/', $rp)) {
|
||||||
|
config_writer_stage_privileged_flat(
|
||||||
|
'/root/.Remote Control', 'password', $rp
|
||||||
|
);
|
||||||
|
config_writer_commit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_exec('sudo mount -o remount,ro / 2>&1');
|
||||||
|
|
||||||
|
// ----- Service start ---------------------------------------
|
||||||
|
$output .= "Starting Services.\n";
|
||||||
|
$start_services = array(
|
||||||
|
'dstarrepeater.service', 'mmdvmhost.service',
|
||||||
|
'ircddbgateway.service', 'timeserver.service',
|
||||||
|
'pistar-watchdog.service', 'pistar-remote.service',
|
||||||
|
);
|
||||||
|
foreach ($start_services as $svc) {
|
||||||
|
shell_exec('sudo systemctl start ' . escapeshellarg($svc) . ' 2>&1');
|
||||||
|
}
|
||||||
|
if (substr(exec('grep "pistar-upnp.service" /etc/crontab | cut -c 1'), 0, 1) !== '#') {
|
||||||
|
shell_exec('sudo systemctl start pistar-upnp.service 2>&1');
|
||||||
|
}
|
||||||
|
$start_services_after_upnp = array(
|
||||||
|
'ysfgateway.service', 'ysf2dmr.service', 'p25gateway.service',
|
||||||
|
'nxdngateway.service', 'm17gateway.service',
|
||||||
|
'dapnetgateway.service', 'mobilegps.service', 'cron.service',
|
||||||
|
);
|
||||||
|
foreach ($start_services_after_upnp as $svc) {
|
||||||
|
shell_exec('sudo systemctl start ' . escapeshellarg($svc) . ' 2>&1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup: remove the staged extraction dir so subsequent
|
||||||
|
// restores don't see leftovers.
|
||||||
|
shell_exec("sudo rm -rf " . escapeshellarg($target_dir) . " 2>&1");
|
||||||
|
|
||||||
|
$output .= "Configuration Restore Complete.\n";
|
||||||
|
echo "<tr><td align=\"left\"><pre>"
|
||||||
|
. htmlspecialchars($output, ENT_QUOTES, 'UTF-8') . "</pre></td></tr>\n";
|
||||||
|
|
||||||
|
} while (false); // end do/while(false) early-exit block
|
||||||
|
};
|
||||||
|
|
||||||
|
echo "</table>\n";
|
||||||
|
} else { ?>
|
||||||
|
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" enctype="multipart/form-data">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2"><?php echo $lang['backup_restore'];?></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="50%">Download Configuration<br />
|
||||||
|
<button style="border: none; background: none;" name="action" value="download"><img src="/images/download.png" border="0" alt="Download Config" /></button>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top">Restore Configuration<br />
|
||||||
|
<button style="border: none; background: none;" name="action" value="restore"><img src="/images/restore.png" border="0" alt="Restore Config" /></button><br />
|
||||||
|
<input type="file" name="fileToUpload" id="fileToUpload" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="justify">
|
||||||
|
<br />
|
||||||
|
<b>WARNING:</b><br />
|
||||||
|
Editing the files outside of Pi-Star *could* have un-desireable side effects.<br />
|
||||||
|
<br />
|
||||||
|
This backup and restore tool, will backup your config files to a Zip file, and allow you to restore them later<br />
|
||||||
|
either to this Pi-Star or another one.<br />
|
||||||
|
<ul>
|
||||||
|
<li>System Passwords / Dashboard passwords are NOT backed up / restored.</li>
|
||||||
|
<li>Wireless Configuration IS backed up and restored</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MMDVM modem log download.
|
||||||
|
*
|
||||||
|
* Sends today's /var/log/pi-star/MMDVM-YYYY-MM-DD.log (or the
|
||||||
|
* dstarrepeater equivalent) as a download. Picks the right log
|
||||||
|
* filename based on which mode marker exists in /etc/. Streams the
|
||||||
|
* file in 8 KiB chunks, with a User-Agent sniff to translate \n to
|
||||||
|
* \r\n for Windows clients.
|
||||||
|
*
|
||||||
|
* NOTE for the security pass: no setSecurityHeaders() call (binary
|
||||||
|
* download), and no auth check beyond the PHP_SELF guard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/download_modem_log.php") {
|
||||||
|
if (file_exists('/etc/dstar-radio.mmdvmhost')) {
|
||||||
|
$logfile = "/var/log/pi-star/MMDVM-".gmdate('Y-m-d').".log";
|
||||||
|
}
|
||||||
|
elseif (file_exists('/etc/dstar-radio.dstarrepeater')) {
|
||||||
|
if (file_exists("/var/log/pi-star/DStarRepeater-".gmdate('Y-m-d').".log")) {$logfile = "/var/log/pi-star/DStarRepeater-".gmdate('Y-m-d').".log";}
|
||||||
|
if (file_exists("/var/log/pi-star/dstarrepeaterd-".gmdate('Y-m-d').".log")) {$logfile = "/var/log/pi-star/dstarrepeaterd-".gmdate('Y-m-d').".log";}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostNameInfo = exec('cat /etc/hostname');
|
||||||
|
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||||
|
header('Cache-Control: private', false);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
if ($hostNameInfo != "pi-star") {
|
||||||
|
header('Content-Disposition: attachment; filename="Pi-Star_'.$hostNameInfo.'_'.basename($logfile).'";');
|
||||||
|
} else {
|
||||||
|
header('Content-Disposition: attachment; filename="Pi-Star_'.basename($logfile).'";');
|
||||||
|
}
|
||||||
|
header('Content-Length: '.filesize($logfile));
|
||||||
|
header('Accept-Ranges: bytes');
|
||||||
|
|
||||||
|
// User Agent Detection
|
||||||
|
if (strpos($_SERVER['HTTP_USER_AGENT'], 'indows') !== false) {
|
||||||
|
$userAgent = "Windows";
|
||||||
|
} else {
|
||||||
|
$userAgent = "NonWindows";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-flight checks done, send the output.
|
||||||
|
set_time_limit(0);
|
||||||
|
$file = @fopen($logfile,"rb");
|
||||||
|
while(!feof($file)) {
|
||||||
|
if ($userAgent == "Windows") { print(str_replace("\n", "\r\n", @fread($file, 1024*8))); }
|
||||||
|
if ($userAgent == "NonWindows") { print(@fread($file, 1024*8)); }
|
||||||
|
ob_flush();
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok we are done, close the file and clean up.
|
||||||
|
@fclose($file);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
else { die; }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../dstarrepeater/
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/dapnetgateway.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/dapnetgateway using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/dapnetgateway /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/dapnetgateway + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (See edit_mmdvmhost.php for the full
|
||||||
|
* rationale on the install vs cp+chmod+chown migration.)
|
||||||
|
* 4. sudo systemctl restart dapnetgateway.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/dapnetgateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/dapnetgateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart dapnetgateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert editor for /etc/pistar-css.ini — the dashboard theme overrides.
|
||||||
|
*
|
||||||
|
* Same staged-write pattern as the other edit_*.php files except no
|
||||||
|
* daemon restart is needed (CSS is loaded fresh on the next page hit).
|
||||||
|
* Provides a 'Reset to defaults' path that does a `sudo rm -rf` on
|
||||||
|
* /etc/pistar-css.ini inside the mount-rw window — guarded only by a
|
||||||
|
* JS confirm() prompt; flag for the security pass.
|
||||||
|
*
|
||||||
|
* Output of this editor is consumed by css/pistar-css.php,
|
||||||
|
* css/pistar-css-mini.php, and admin/wifi/styles.php — the three CSS
|
||||||
|
* emitters that read from /etc/pistar-css.ini.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<script type="text/javascript">
|
||||||
|
function factoryReset()
|
||||||
|
{
|
||||||
|
// Typed confirmation. The server-side handler requires
|
||||||
|
// factoryResetConfirm === 'RESET' before performing the
|
||||||
|
// wipe — so a misclicked button or an accidental form
|
||||||
|
// replay does NOT reset the dashboard CSS, even though
|
||||||
|
// the CSRF token is otherwise valid.
|
||||||
|
var typed = prompt(
|
||||||
|
'WARNING: This will reset these settings to factory defaults.\n\n'
|
||||||
|
+ 'Type RESET (uppercase) and press OK to proceed.\n'
|
||||||
|
+ 'Press Cancel to go back.', '');
|
||||||
|
if (typed === null) { return false; } // Cancel
|
||||||
|
if (typed !== 'RESET') {
|
||||||
|
alert('Confirmation text did not match. Factory reset cancelled.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
document.getElementById('factoryResetConfirmInput').value = typed;
|
||||||
|
document.getElementById('factoryReset').submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() up front so both the factory-init branch (when
|
||||||
|
// /etc/pistar-css.ini doesn't exist yet) and the normal-load
|
||||||
|
// branch use the same per-request random staging path.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
if (!file_exists('/etc/pistar-css.ini')) {
|
||||||
|
//The source file does not exist, lets create it....
|
||||||
|
$outFile = fopen($filepath, "w") or die("Unable to open file!");
|
||||||
|
$fileContent = "[Background]\nPage=edf0f5\nContent=ffffff\nBanners=dd4b39\n\n";
|
||||||
|
$fileContent .= "[Text]\nBanners=ffffff\nBannersDrop=303030\n\n";
|
||||||
|
$fileContent .= "[Tables]\nHeadDrop=8b0000\nBgEven=f7f7f7\nBgOdd=d0d0d0\n\n";
|
||||||
|
$fileContent .= "[Content]\nText=000000\n\n";
|
||||||
|
$fileContent .= "[BannerH1]\nEnabled=0\nText=\"Some Text\"\n\n";
|
||||||
|
$fileContent .= "[BannerExtText]\nEnabled=0\nText=\"Some long text entry\"\n\n";
|
||||||
|
$fileContent .= "[Lookup]\nService=\"RadioID\"\n";
|
||||||
|
fwrite($outFile, $fileContent);
|
||||||
|
fclose($outFile);
|
||||||
|
|
||||||
|
// Atomic install: content + mode + owner set in one syscall
|
||||||
|
// sequence. Replaces the prior cp + chmod + chown trio so an
|
||||||
|
// interrupted RW window can't leave /etc/pistar-css.ini at the
|
||||||
|
// staging file's www-data:www-data 600. The dashboard's CSS
|
||||||
|
// renderer (/css/pistar-css.php) reads it via parse_ini_file
|
||||||
|
// without sudo, so 644 root:root preserves the read path.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/pistar-css.ini');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Do some file wrangling...
|
||||||
|
exec('sudo cp /etc/pistar-css.ini ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
//after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
// Factory Reset Handler Here
|
||||||
|
if (empty($_POST['factoryReset']) != TRUE ) {
|
||||||
|
// Server-side confirmation gate. The form ships a hidden
|
||||||
|
// factoryResetConfirm input that the JS factoryReset()
|
||||||
|
// populates only after the operator types `RESET` into
|
||||||
|
// the prompt. Comparing strictly to the magic string
|
||||||
|
// (===) means a misclicked button, a replayed POST, or
|
||||||
|
// a curl with just `factoryReset=1` does NOT trigger the
|
||||||
|
// wipe — even with a valid CSRF token.
|
||||||
|
$confirm = isset($_POST['factoryResetConfirm']) ? $_POST['factoryResetConfirm'] : '';
|
||||||
|
if ($confirm !== 'RESET') {
|
||||||
|
echo "<br />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th>Factory Reset NOT performed</th></tr>\n";
|
||||||
|
echo "<tr><td>Server-side confirmation did not match. Factory reset cancelled.</td><tr>\n";
|
||||||
|
echo "</table>\n";
|
||||||
|
unset($_POST);
|
||||||
|
} else {
|
||||||
|
echo "<br />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th>Factory Reset Config</th></tr>\n";
|
||||||
|
echo "<tr><td>Loading fresh configuration file(s)...</td><tr>\n";
|
||||||
|
echo "</table>\n";
|
||||||
|
unset($_POST);
|
||||||
|
//Reset the config
|
||||||
|
exec('sudo mount -o remount,rw /'); // Make rootfs writable
|
||||||
|
exec('sudo rm -rf /etc/pistar-css.ini'); // Delete the Config
|
||||||
|
exec('sudo mount -o remount,ro /'); // Make rootfs read-only
|
||||||
|
echo '<script type="text/javascript">setTimeout(function() { window.location=window.location;},0);</script>';
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
//parse the ini file to get the sections
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$section = str_replace("BannerH2", "BannerH1", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."=none\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
//write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Atomic install — see the matching block earlier in this
|
||||||
|
// file for the rationale. Single sudo call, content + mode
|
||||||
|
// + owner all set together; no transient state on disk.
|
||||||
|
// $filepath here is the function parameter, which is the
|
||||||
|
// same per-request tempnam path created at file-top (A3-3).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/pistar-css.ini');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
if (isset($parsed_ini['Lookup']['popupWidth'])) { unset($parsed_ini['Lookup']['popupWidth']); }
|
||||||
|
if (isset($parsed_ini['Lookup']['popupHeight'])) { unset($parsed_ini['Lookup']['popupHeight']); }
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// Same hardening as edit_mmdvmhost.php (#23): escape every
|
||||||
|
// INI section / key / value before HTML interpolation. The
|
||||||
|
// save handler writes POST bytes verbatim so legitimate
|
||||||
|
// values (including any with `"` or `<`) round-trip
|
||||||
|
// byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
if ( $section == "Lookup" && $key == "Service" ) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\">\n";
|
||||||
|
echo " <select name=\"{$sectionHtml}[$keyHtml]\" />\n";
|
||||||
|
if ($value == "RadioID") {
|
||||||
|
echo " <option value=\"RadioID\" selected=\"selected\">RadioID Callsign Lookup</option>\n";
|
||||||
|
} else {
|
||||||
|
echo " <option value=\"RadioID\">RadioID Callsign Lookup</option>\n";
|
||||||
|
}
|
||||||
|
if ($value == "QRZ") {
|
||||||
|
echo " <option value=\"QRZ\" selected=\"selected\">QRZ Callsign Lookup</option>\n";
|
||||||
|
} else {
|
||||||
|
echo " <option value=\"QRZ\">QRZ Callsign Lookup</option>\n";
|
||||||
|
}
|
||||||
|
echo " </select>\n";
|
||||||
|
echo "</td></tr>\n";
|
||||||
|
} else {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br /><br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
echo "<br />\n";
|
||||||
|
echo 'if you took it all too far and now it makes you feel sick, click below to reset the changes made on this page, this will ONLY reset the CSS settings above and will not change any other settings or configuration.'."\n";
|
||||||
|
echo '<form id="factoryReset" action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
echo ' <div><input type="hidden" name="factoryReset" value="1" /></div>'."\n";
|
||||||
|
// Server-side confirmation. JS factoryReset() prompts for the
|
||||||
|
// magic word and only populates this input on a match. The
|
||||||
|
// handler requires factoryResetConfirm === 'RESET' before doing
|
||||||
|
// the wipe — closes the "valid CSRF token + accidental replay"
|
||||||
|
// attack class.
|
||||||
|
echo ' <div><input type="hidden" id="factoryResetConfirmInput" name="factoryResetConfirm" value="" /></div>'."\n";
|
||||||
|
echo '</form>'."\n";
|
||||||
|
echo '<input type="button" onclick="javascript:factoryReset();" value="'.$lang['factory_reset'].'" />'."\n";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/dmrgateway.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/dmrgateway using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/dmrgateway /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/dmrgateway + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (See edit_mmdvmhost.php for the full
|
||||||
|
* rationale on the install vs cp+chmod+chown migration.)
|
||||||
|
* 4. sudo systemctl restart dmrgateway.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
//Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/dmrgateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
//after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
//parse the ini file to get the sections
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if (($section == "DMR Network 1" || $section == "DMR Network 2") && $key == "Password" && $value) {
|
||||||
|
$value = str_replace('"', "", $value);
|
||||||
|
$content .= $key."=\"".$value."\"\n";
|
||||||
|
} elseif (($section == "DMR Network 1" || $section == "DMR Network 2") && $key == "Options" && $value) {
|
||||||
|
$value = str_replace('"', "", $value);
|
||||||
|
$content .= $key."=\"".$value."\"\n";
|
||||||
|
} else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
//write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/dmrgateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart dmrgateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert editor for /etc/dstarrepeater (D-Star modem driver config).
|
||||||
|
*
|
||||||
|
* The on-disk file is a flat key=value (no [section] header), but the
|
||||||
|
* editor parses it via parse_ini_file() — so we synthesise a temporary
|
||||||
|
* `[dstarrepeater]` header on read and `sed -i` it back out before the
|
||||||
|
* file is committed. Otherwise follows the standard Pi-Star
|
||||||
|
* copy-via-/tmp / mount-rw / restart pattern; daemon:
|
||||||
|
* dstarrepeater.service.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() creates the staging file mode 600 owned by www-data
|
||||||
|
// with an unguessable random suffix; cleanup is registered up
|
||||||
|
// front so the file never persists past script exit.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/dstarrepeater ' . escapeshellarg($filepath));
|
||||||
|
// Defensive re-assert; tempnam already 600 www-data and `sudo cp`
|
||||||
|
// preserves it, but match the surrounding pattern as belt-and-braces.
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
// Mangle the input
|
||||||
|
$file_content = "[dstarrepeater]\n".preg_replace('~\r\n?~', "\n", file_get_contents($filepath));
|
||||||
|
file_put_contents($filepath, $file_content);
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."= \n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// /etc/dstarrepeater is a FLAT key=value file on disk — the
|
||||||
|
// synthetic [dstarrepeater] header is injected only for
|
||||||
|
// parse_ini_file()'s benefit. Strip it via PHP and install
|
||||||
|
// the cleaned content directly (L-7: drops sudo sed -i;
|
||||||
|
// L-5: collapses cp + chmod + chown into one atomic install).
|
||||||
|
// See edit_ircddbgateway.php for the full rationale.
|
||||||
|
$etcContent = preg_replace('/^\[dstarrepeater\]\r?\n/m', '', $content);
|
||||||
|
// A3-3: per-request random staging — see edit_ircddbgateway.php
|
||||||
|
$etcStaging = tempnam('/tmp', 'pistar-edit-etc-');
|
||||||
|
file_put_contents($etcStaging, $etcContent);
|
||||||
|
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($etcStaging) . ' /etc/dstarrepeater');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
@unlink($etcStaging);
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart dstarrepeater.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert editor for /etc/ircddbgateway (D-Star side gateway config).
|
||||||
|
*
|
||||||
|
* Same flat key=value file as /etc/dstarrepeater — uses the synthetic
|
||||||
|
* `[ircddbgateway]` section header trick on read and sed-strip on
|
||||||
|
* write so parse_ini_file() can handle it. Standard Pi-Star
|
||||||
|
* copy-via-/tmp / mount-rw / restart pattern; daemon:
|
||||||
|
* ircddbgateway.service.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Stage a copy of /etc/ircddbgateway in /tmp under a random,
|
||||||
|
// per-request name (A3-3). tempnam() creates the file mode 600
|
||||||
|
// owned by the calling PHP-FPM user (www-data); the unguessable
|
||||||
|
// suffix defeats the predictable-name TOCTOU class — an attacker
|
||||||
|
// who knew the path could otherwise pre-create it as a symlink
|
||||||
|
// to /etc/shadow or similar and have our `sudo cp` follow the
|
||||||
|
// link and overwrite the target. Cleanup is registered up front
|
||||||
|
// so the staging copy never persists past script exit, even on a
|
||||||
|
// die() / fatal-error path. @-suppression handles the case where
|
||||||
|
// a sudo mv path consumed the file before script end.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/ircddbgateway ' . escapeshellarg($filepath));
|
||||||
|
// Defensively re-assert mode + owner. tempnam already created
|
||||||
|
// the file 600 www-data, and `sudo cp` against an existing
|
||||||
|
// regular file truncates-in-place (mode/owner preserved); these
|
||||||
|
// remain as belt-and-braces to match the surrounding pattern.
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
// Mangle the input
|
||||||
|
$file_content = "[ircddbgateway]\n".preg_replace('~\r\n?~', "\n", file_get_contents($filepath));
|
||||||
|
file_put_contents($filepath, $file_content);
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."= \n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// /etc/ircddbgateway is a FLAT key=value file on disk — the
|
||||||
|
// synthetic [ircddbgateway] header was injected at line ~75
|
||||||
|
// only to satisfy parse_ini_file()'s section model. The /tmp
|
||||||
|
// staging file keeps the header (so the form re-render via
|
||||||
|
// parse_ini_file at the bottom of this script still finds
|
||||||
|
// sections); the on-disk version must not. Strip via PHP's
|
||||||
|
// preg_replace and install the cleaned content directly —
|
||||||
|
// drops the prior `sudo sed -i` from this code path (L-7).
|
||||||
|
$etcContent = preg_replace('/^\[ircddbgateway\]\r?\n/m', '', $content);
|
||||||
|
// A3-3: per-request random staging file rather than a
|
||||||
|
// predictable hardcoded /tmp/<obf>.tmp path. tempnam() also
|
||||||
|
// creates the file mode 600 — and since this is a freshly
|
||||||
|
// created file (not yet referenced anywhere), there's no
|
||||||
|
// race for an attacker to swap in a symlink before our write.
|
||||||
|
$etcStaging = tempnam('/tmp', 'pistar-edit-etc-');
|
||||||
|
file_put_contents($etcStaging, $etcContent);
|
||||||
|
|
||||||
|
// Atomic install: content + mode + owner set in one syscall
|
||||||
|
// sequence (B5 / L-5 pattern). Replaces the prior cp +
|
||||||
|
// chmod + chown trio so an interrupted RW window can't leave
|
||||||
|
// /etc/ircddbgateway in a transient state.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($etcStaging) . ' /etc/ircddbgateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
@unlink($etcStaging);
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart ircddbgateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/mmdvmhost.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/mmdvmhost using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/mmdvmhost /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/mmdvmhost + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (L-5: collapses the prior cp + chmod +
|
||||||
|
* chown triplet into one atomic call — same idiom used by
|
||||||
|
* configure.php's gateway-save block. The triplet would also
|
||||||
|
* be rejected by the tightened /etc/sudoers.d/pistar-dashboard
|
||||||
|
* because it allowlists install but not bare cp+chmod+chown
|
||||||
|
* against /etc/<gateway>.)
|
||||||
|
* 4. sudo systemctl restart mmdvmhost.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
//Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() creates the staging file mode 600 owned by www-data
|
||||||
|
// with an unguessable random suffix.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/mmdvmhost ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
//after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
//parse the ini file to get the sections
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($section == "DMR Network" && $key == "Password" && $value) {
|
||||||
|
$value = str_replace('"', "", $value);
|
||||||
|
$content .= $key."=\"".$value."\"\n";
|
||||||
|
}
|
||||||
|
elseif ($section == "DMR Network" && $key == "Options" && $value) {
|
||||||
|
$value = str_replace('"', "", $value);
|
||||||
|
$content .= $key."=\"".$value."\"\n";
|
||||||
|
}
|
||||||
|
elseif ($section == "DMR Network" && $key == "Options" && !$value) {
|
||||||
|
$content .= $key."= \n";
|
||||||
|
}
|
||||||
|
elseif ($value == '') {
|
||||||
|
$content .= $key."=none\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
//write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Updates complete - install the working file back to the
|
||||||
|
// proper location. L-5: atomic install replaces the prior
|
||||||
|
// cp + chmod + chown triplet (which the tightened
|
||||||
|
// /etc/sudoers.d/pistar-dashboard rejects on the bare
|
||||||
|
// chmod/chown against /etc/<file> — only the install pattern
|
||||||
|
// is allowlisted there).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/mmdvmhost');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart mmdvmhost.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// INI section / key / value all come from /etc/mmdvmhost.
|
||||||
|
// Operator-trusted in principle, but the M-3 audit class
|
||||||
|
// means a value with a literal `"` or `<` (legitimate INI
|
||||||
|
// content — e.g. "Options=foo=1,bar" or a callsign with a
|
||||||
|
// space) breaks out of `value="…"` attributes and renders
|
||||||
|
// as HTML.
|
||||||
|
//
|
||||||
|
// htmlspecialchars(ENT_QUOTES) preserves round-trip safety:
|
||||||
|
// on display: `"` -> `"`, `<` -> `<` etc.
|
||||||
|
// browser decode (form parse): `"` -> `"` again.
|
||||||
|
// POST: the raw byte gets back to the save handler.
|
||||||
|
// save handler at line ~108: writes `key=value` to INI
|
||||||
|
// verbatim — no further escaping or unescaping.
|
||||||
|
// So values like `Options="foo=1,bar"` go in, get displayed
|
||||||
|
// as `Options="foo=1,bar"`, the browser still shows
|
||||||
|
// `"foo=1,bar"` in the input field, and re-saving produces
|
||||||
|
// a byte-identical INI line.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
if (($key == "Options") || ($value)) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
elseif (($key == "Display") && ($value == '')) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"None\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"0\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/nxdngateway.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/nxdngateway using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/nxdngateway /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/nxdngateway + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (See edit_mmdvmhost.php for the full
|
||||||
|
* rationale on the install vs cp+chmod+chown migration.)
|
||||||
|
* 4. sudo systemctl restart nxdngateway.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/nxdngateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
if (strpos($section, 'aprs') !== false) { $section = str_replace("_", ".", $section); }
|
||||||
|
else { $section = str_replace("_", " ", $section); $section = str_replace(".", " ", $section); }
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/nxdngateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart nxdngateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/p25gateway.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/p25gateway using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/p25gateway /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/p25gateway + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (See edit_mmdvmhost.php for the full
|
||||||
|
* rationale on the install vs cp+chmod+chown migration.)
|
||||||
|
* 4. sudo systemctl restart p25gateway.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/p25gateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/p25gateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart p25gateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert editor for /etc/starnetserver (D-Star StarNet groups config).
|
||||||
|
*
|
||||||
|
* Synthetic `[starnetserver]` section header trick on read like
|
||||||
|
* edit_ircddbgateway.php / edit_dstarrepeater.php. Standard Pi-Star
|
||||||
|
* copy-via-/tmp / mount-rw write pattern.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() creates the staging file mode 600 owned by www-data
|
||||||
|
// with an unguessable random suffix; cleanup is registered up
|
||||||
|
// front so the file never persists past script exit.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/starnetserver ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
// Mangle the input
|
||||||
|
$file_content = "[starnetserver]\n".file_get_contents($filepath);
|
||||||
|
file_put_contents($filepath, $file_content);
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."=".$value." \n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// /etc/starnetserver is a FLAT key=value file on disk — the
|
||||||
|
// synthetic [starnetserver] header is injected only for
|
||||||
|
// parse_ini_file()'s benefit. Strip it via PHP and install
|
||||||
|
// the cleaned content directly (L-7: drops sudo sed -i;
|
||||||
|
// L-5: collapses cp + chmod + chown into one atomic install).
|
||||||
|
// See edit_ircddbgateway.php for the full rationale.
|
||||||
|
$etcContent = preg_replace('/^\[starnetserver\]\r?\n/m', '', $content);
|
||||||
|
// A3-3: per-request random staging — see edit_ircddbgateway.php
|
||||||
|
$etcStaging = tempnam('/tmp', 'pistar-edit-etc-');
|
||||||
|
file_put_contents($etcStaging, $etcContent);
|
||||||
|
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($etcStaging) . ' /etc/starnetserver');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
@unlink($etcStaging);
|
||||||
|
|
||||||
|
// Reload the affected daemon so the saved edits take effect
|
||||||
|
// without a manual restart. Was commented out historically;
|
||||||
|
// restored so behaviour matches the other edit_*.php files.
|
||||||
|
exec('sudo systemctl restart starnetserver.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert editor for /etc/timeserver (D-Star time-announcement config).
|
||||||
|
*
|
||||||
|
* Flat key=value file like ircddbgateway / dstarrepeater — uses the
|
||||||
|
* synthetic `[timeserver]` section header trick on read with a CR/LF
|
||||||
|
* normalisation step before sed-strip on write. Standard Pi-Star
|
||||||
|
* copy-via-/tmp / mount-rw / restart pattern; daemon:
|
||||||
|
* timeserver.service.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() creates the staging file mode 600 owned by www-data
|
||||||
|
// with an unguessable random suffix; cleanup is registered up
|
||||||
|
// front so the file never persists past script exit.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/timeserver ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
// Mangle the input
|
||||||
|
$file_content = "[timeserver]\n".preg_replace('~\r\n?~', "\n", file_get_contents($filepath));
|
||||||
|
file_put_contents($filepath, $file_content);
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."= \n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// /etc/timeserver is a FLAT key=value file on disk — the
|
||||||
|
// synthetic [timeserver] header is injected only for
|
||||||
|
// parse_ini_file()'s benefit. Strip it via PHP and install
|
||||||
|
// the cleaned content directly (L-7: drops sudo sed -i;
|
||||||
|
// L-5: collapses cp + chmod + chown into one atomic install).
|
||||||
|
// See edit_ircddbgateway.php for the full rationale.
|
||||||
|
$etcContent = preg_replace('/^\[timeserver\]\r?\n/m', '', $content);
|
||||||
|
// A3-3: per-request random staging — see edit_ircddbgateway.php
|
||||||
|
$etcStaging = tempnam('/tmp', 'pistar-edit-etc-');
|
||||||
|
file_put_contents($etcStaging, $etcContent);
|
||||||
|
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($etcStaging) . ' /etc/timeserver');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
@unlink($etcStaging);
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart timeserver.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="Save Changes" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert INI editor for /etc/ysfgateway.
|
||||||
|
*
|
||||||
|
* Renders a per-section / per-key form built from parse_ini_file()
|
||||||
|
* output, accepts edits via POST, then writes the result back to
|
||||||
|
* /etc/ysfgateway using the standard Pi-Star copy-via-/tmp pattern:
|
||||||
|
* 1. sudo cp /etc/ysfgateway /tmp/<obfuscated>.tmp + chown www-data
|
||||||
|
* + chmod 600 (so PHP can edit the temp).
|
||||||
|
* 2. fopen('w') and fwrite the rebuilt INI text into the temp.
|
||||||
|
* 3. sudo mount -o remount,rw / + sudo install -m 644 -o root -g
|
||||||
|
* root temp -> /etc/ysfgateway + sudo mount -o remount,ro / to
|
||||||
|
* seal the rootfs again. (See edit_mmdvmhost.php for the full
|
||||||
|
* rationale on the install vs cp+chmod+chown migration.)
|
||||||
|
* 4. sudo systemctl restart ysfgateway.service to pick up the change.
|
||||||
|
*
|
||||||
|
* Admin-only access; the dashboard's Apache basic-auth gate is the
|
||||||
|
* sole protection. The validation is what's in the form (none, in
|
||||||
|
* effect — operator-typed values are written raw). Treat with care.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Do some file wrangling...
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
exec('sudo cp /etc/ysfgateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
// Clean up the /tmp staging file on script exit so the
|
||||||
|
// editor's potentially-secrets-bearing copy of /etc/<config>
|
||||||
|
// doesn't persist between requests. @-suppression handles
|
||||||
|
// the case where a sudo mv (e.g. fulledit_bmapikey) already
|
||||||
|
// consumed the staging file before script end.
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
|
||||||
|
// after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
// parse the ini file to get the sections
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
if (strpos($section, 'aprs') !== false) { $section = str_replace("_", ".", $section); }
|
||||||
|
else { $section = str_replace("_", " ", $section); $section = str_replace(".", " ", $section); }
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/ysfgateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart ysfgateway.service'); // Reload the daemon
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
// INI section / key / value all come from the underlying
|
||||||
|
// /etc/<gateway> file. Same hardening as edit_mmdvmhost.php
|
||||||
|
// (#23): htmlspecialchars(ENT_QUOTES) on display so a value
|
||||||
|
// with a literal `"` or `<` (e.g. an Options string) can't
|
||||||
|
// break out of the `value="…"` attribute. The save handler
|
||||||
|
// writes the POST bytes verbatim, so legitimate quoted
|
||||||
|
// values round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/bmapi.key (BrandMeister API token).
|
||||||
|
*
|
||||||
|
* Creates the file on first save if it doesn't exist, using a sudo
|
||||||
|
* shell-redirected `echo` (slight deviation from the standard
|
||||||
|
* staged-write pattern). Used by mmdvmhost/bm_links.php and
|
||||||
|
* mmdvmhost/bm_manager.php as the Bearer token for BrandMeister
|
||||||
|
* API queries. No daemon restart.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// /etc/bmapi.key holds the BrandMeister API token, so the
|
||||||
|
// random-name TOCTOU defence is more important here than for the
|
||||||
|
// other editors. tempnam() creates the staging file mode 600
|
||||||
|
// owned by www-data with an unguessable random suffix.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
if (file_exists('/etc/bmapi.key')) {
|
||||||
|
exec('sudo cp /etc/bmapi.key ' . escapeshellarg($filepath));
|
||||||
|
} else {
|
||||||
|
// Seed the staging file with the empty-config default. tempnam
|
||||||
|
// already created the file owned by www-data, so PHP-side
|
||||||
|
// file_put_contents writes through directly — no `sudo echo`
|
||||||
|
// gymnastics needed.
|
||||||
|
file_put_contents($filepath, "[key]\napikey=None\n");
|
||||||
|
}
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
//after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
// Strip CR/LF from values before they reach the INI
|
||||||
|
// line. Same rationale as fulledit_dapnetapi.php: a
|
||||||
|
// newline inside $value would split into a fresh
|
||||||
|
// INI line and let an attacker inject extra keys.
|
||||||
|
$value = str_replace(array("\r", "\n"), "", (string)$value);
|
||||||
|
if ($value == '') {
|
||||||
|
$content .= $key."=none\n";
|
||||||
|
} else {
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
//write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Atomic install: mode + owner set in one syscall sequence.
|
||||||
|
// /etc/bmapi.key holds the BrandMeister API token — mode 600
|
||||||
|
// keeps it readable only by www-data (the dashboard user).
|
||||||
|
// Owner left as www-data because banner_warnings.inc / bm_links.php /
|
||||||
|
// bm_manager.php read the file directly via parse_ini_file()
|
||||||
|
// without sudo — switching to root:root here would silently
|
||||||
|
// break those reads. (Tightening to root:root is a follow-up
|
||||||
|
// once the read sites move to a sudo-cat helper.)
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 600 -o www-data -g www-data '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/bmapi.key');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
if (!isset($parsed_ini['key']['apikey'])) { $parsed_ini['key']['apikey'] = ""; }
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// INI section / key / value all come from /etc/bmapi.key. Same
|
||||||
|
// hardening as edit_mmdvmhost.php (#23) but the value lands
|
||||||
|
// inside a <textarea>...</textarea> body — htmlspecialchars
|
||||||
|
// covers both attribute and body contexts safely. Browser
|
||||||
|
// decodes the named entities on form submit, so legitimate
|
||||||
|
// values containing `<`, `>`, `&`, `"` round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
if (($key == "Options") || ($value)) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><textarea name=\"{$sectionHtml}[$keyHtml]\" cols=\"60\" rows=\"13\">$valueHtml</textarea></td></tr>\n";
|
||||||
|
}
|
||||||
|
elseif (($key == "Display") && ($value == '')) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><textarea name=\"{$sectionHtml}[$keyHtml]\" cols=\"60\" rows=\"13\">$valueHtml</textarea></td></tr>\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><textarea name=\"{$sectionHtml}[$keyHtml]\" cols=\"60\" rows=\"13\">$valueHtml</textarea></td></tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/crontab.
|
||||||
|
*
|
||||||
|
* Drops the parse_ini_file dance — the file is a plain text crontab,
|
||||||
|
* not an INI. POST data lands in a textarea, gets staged to
|
||||||
|
* /tmp/<obfuscated>.tmp, then sudo-copied back into /etc/crontab.
|
||||||
|
* No daemon restart (cron rereads its config automatically).
|
||||||
|
*
|
||||||
|
* Operator can edit any cron line, including the pistar-* timer hooks
|
||||||
|
* — read carefully before saving.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Cron-content denylist scanner.
|
||||||
|
*
|
||||||
|
* The dashboard sits behind HTTP basic auth. An operator (or anyone
|
||||||
|
* with the basic-auth credential) can edit /etc/crontab through this
|
||||||
|
* page. Cron runs lines as root, so a malicious cron entry is a
|
||||||
|
* direct privilege escalation off whatever access the dashboard
|
||||||
|
* already grants.
|
||||||
|
*
|
||||||
|
* This is BASIC HARDENING — not airtight. A determined attacker can
|
||||||
|
* still bypass via:
|
||||||
|
*
|
||||||
|
* - Variable indirection: `* * * * * root $X user` where $X is
|
||||||
|
* defined in a sourced file.
|
||||||
|
* - External script paths: a benign-looking `/opt/myapp/run.sh`
|
||||||
|
* can do anything; we scan the cron line, not the script.
|
||||||
|
* - Brace expansion: `/usr/bin/{passwd,sh}` produces `passwd`
|
||||||
|
* after shell expansion but our `\b` boundary doesn't trip on
|
||||||
|
* the literal `passwd,` substring.
|
||||||
|
*
|
||||||
|
* Obfuscation patterns are NOT an accepted bypass — there's no
|
||||||
|
* legitimate cron use for inline base64 / hex / eval / etc., so
|
||||||
|
* the second rule block below blocks every common decoder
|
||||||
|
* (base64 / b64decode / fromhex / xxd / openssl -d / shell hex
|
||||||
|
* escapes / shell unicode escapes / eval). Operators with a
|
||||||
|
* legitimate need to encode/decode data should put that logic in
|
||||||
|
* a script file, where this denylist doesn't apply.
|
||||||
|
*
|
||||||
|
* The goal is to catch the obvious-bad patterns described in the
|
||||||
|
* threat brief: password mutation, direct writes to /etc/shadow /
|
||||||
|
* /etc/passwd / .htpasswd, and the standard shell-listener /
|
||||||
|
* reverse-shell idioms (nc, socat exec, /dev/tcp redirects,
|
||||||
|
* download-pipe-shell, python/perl one-liner shells), plus the
|
||||||
|
* obfuscation patterns commonly used to wrap them.
|
||||||
|
*
|
||||||
|
* Comment lines (starting with `#`) and blank lines are exempt.
|
||||||
|
* Cron variable assignments (PATH=, MAILTO=, SHELL=) are scanned
|
||||||
|
* the same as commands — the rules below tolerate the false-positive
|
||||||
|
* surface for the safety win. Known accepted false positive in the
|
||||||
|
* wild: `MAILTO=passwd@example.com` triggers the password-command
|
||||||
|
* rule. Operators with that exact email can use a different alias
|
||||||
|
* or edit /etc/crontab via SSH to bypass the dashboard guard.
|
||||||
|
*
|
||||||
|
* Returns an array of human-readable diagnostics, one per problem
|
||||||
|
* line. Empty array means OK to save.
|
||||||
|
*
|
||||||
|
* @param string $content Raw POSTed cron content (already \r-stripped
|
||||||
|
* and decoded back to bytes by the browser).
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function pistar_cron_validate($content)
|
||||||
|
{
|
||||||
|
$rules = array(
|
||||||
|
// Password modification commands. The leading character class
|
||||||
|
// matches start-of-line OR a typical token boundary (whitespace,
|
||||||
|
// path separator, shell metas, or `=` so we catch X=passwd).
|
||||||
|
// \b at the end stops "passwords" / "chpasswdtest" matching.
|
||||||
|
'#(?:^|[\s/;|&`$()=])(passwd|chpasswd|htpasswd)\b#i'
|
||||||
|
=> 'invokes a password-modification command (passwd / chpasswd / htpasswd)',
|
||||||
|
// usermod -p sets a password hash directly, no PAM, no audit.
|
||||||
|
'#\busermod\b[^\n]*\s-p\b#i'
|
||||||
|
=> 'usermod -p sets a password hash directly',
|
||||||
|
// Reference to any auth file. Even reading these from cron is
|
||||||
|
// unusual; writing is highly suspicious. Catches /etc/shadow,
|
||||||
|
// /etc/passwd, /etc/passwd-, and any .htpasswd path including
|
||||||
|
// /var/www/.htpasswd.
|
||||||
|
'#(/etc/shadow|/etc/passwd|\.htpasswd)\b#i'
|
||||||
|
=> 'references an authentication file path',
|
||||||
|
// Netcat — block any invocation. Operators with a legitimate
|
||||||
|
// use should script it outside cron. Blocking outright is
|
||||||
|
// simpler than disambiguating safe vs. unsafe nc flags.
|
||||||
|
'#(?:^|[\s/;|&`$()=])(nc|ncat|netcat)\b#i'
|
||||||
|
=> 'invokes nc / ncat / netcat',
|
||||||
|
// socat with EXEC: or SYSTEM: targets — process-spawning.
|
||||||
|
'#\bsocat\b[^\n]*\b(EXEC|SYSTEM):#i'
|
||||||
|
=> 'socat with EXEC/SYSTEM token (spawns a process)',
|
||||||
|
// Bash builtin /dev/tcp / /dev/udp redirect — classic reverse-
|
||||||
|
// shell idiom (`bash -i >& /dev/tcp/h/p 0>&1`). No legitimate
|
||||||
|
// cron use.
|
||||||
|
'#/dev/(?:tcp|udp)/#'
|
||||||
|
=> 'reads or writes /dev/tcp/* or /dev/udp/* (reverse-shell pattern)',
|
||||||
|
// Download-pipe-shell. Catches `curl ... | sh`, `wget ... | bash`,
|
||||||
|
// etc. The `[^|\n]*` between the fetch tool and the pipe stops
|
||||||
|
// a stray pipe later in a long line giving false positives.
|
||||||
|
'#\b(curl|wget|fetch)\b[^|\n]*\|\s*(?:bash|sh|zsh|ksh|dash|exec)\b#i'
|
||||||
|
=> 'pipes downloaded content directly into a shell',
|
||||||
|
// python reverse-shell one-liners — heuristic on imports/calls
|
||||||
|
// commonly used in the published payloads.
|
||||||
|
'#\bpython[23]?\b[^\n]*-c[^\n]*(?:socket\.socket|os\.dup2)#i'
|
||||||
|
=> 'python reverse-shell pattern',
|
||||||
|
// perl reverse-shell one-liner — `perl -e \'use Socket; ...\'`
|
||||||
|
'#\bperl\b[^\n]*-e[^\n]*\bSocket\b#i'
|
||||||
|
=> 'perl reverse-shell pattern',
|
||||||
|
|
||||||
|
// ====== Obfuscation / decoder patterns ======
|
||||||
|
// No legitimate cron use for inline encoded payloads. Operators
|
||||||
|
// who need to encode/decode something should do it in a script,
|
||||||
|
// not in a cron line. Catches the common payload-wrapper idioms.
|
||||||
|
|
||||||
|
// base64 in any form — the command, the python module, perl's
|
||||||
|
// decode_base64, etc. Substring match (no \b) so it also
|
||||||
|
// catches "decode_base64", "MIME::Base64", "base64.b64decode".
|
||||||
|
'#base64#i'
|
||||||
|
=> 'references base64 (no legitimate cron use; payload-obfuscation idiom)',
|
||||||
|
// python's base64 module exposes b64decode / b64encode aliases
|
||||||
|
// that don't contain the literal "base64" substring.
|
||||||
|
'#\bb64(?:decode|encode)\b#i'
|
||||||
|
=> 'python b64decode/b64encode (payload decoder)',
|
||||||
|
// python's bytes.fromhex(...) — turns a hex string into bytes
|
||||||
|
// for runtime execution.
|
||||||
|
'#\bfromhex\b#i'
|
||||||
|
=> 'fromhex (hex payload decoder)',
|
||||||
|
// xxd dumps / reverses hex. Reverse mode (-r) is a payload
|
||||||
|
// decoder; forward mode is hex-dumping which has no cron use.
|
||||||
|
// Block the binary outright.
|
||||||
|
'#\bxxd\b#i'
|
||||||
|
=> 'invokes xxd (hex dump / reverse-mode payload decoder)',
|
||||||
|
// Shell hex / unicode escape literals. `printf \'\x70\x61\x73\x73\'`
|
||||||
|
// spells "pass" at runtime — pure obfuscation in cron context.
|
||||||
|
'#\\\\x[0-9a-fA-F]{2}#'
|
||||||
|
=> 'shell hex-escape literal (\\xNN — payload obfuscation)',
|
||||||
|
'#\\\\u[0-9a-fA-F]{4}#'
|
||||||
|
=> 'shell unicode-escape literal (\\uNNNN — payload obfuscation)',
|
||||||
|
// eval interprets its argument as shell — opaque to anyone
|
||||||
|
// reading the cron line.
|
||||||
|
'#(?:^|[\s/;|&`$()=])eval\b#i'
|
||||||
|
=> 'invokes eval (opaque shell-string execution)',
|
||||||
|
// openssl -d in any context (enc -d, base64 -d, ...) is a
|
||||||
|
// generic decoder.
|
||||||
|
'#\bopenssl\b[^\n]*\s-d\b#i'
|
||||||
|
=> 'openssl with -d (decoder)',
|
||||||
|
);
|
||||||
|
|
||||||
|
$blockers = array();
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
$trimmed = ltrim($line);
|
||||||
|
if ($trimmed === '' || $trimmed[0] === '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($rules as $pattern => $reason) {
|
||||||
|
if (preg_match($pattern, $line)) {
|
||||||
|
$blockers[] = sprintf(
|
||||||
|
'line %d (%s): %s',
|
||||||
|
$i + 1,
|
||||||
|
$reason,
|
||||||
|
substr(trim($line), 0, 100)
|
||||||
|
);
|
||||||
|
break; // one diagnostic per line is enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $blockers;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cronBlockers = array();
|
||||||
|
$saveError = false;
|
||||||
|
$saveOk = false;
|
||||||
|
$readError = false;
|
||||||
|
if(isset($_POST['data'])) {
|
||||||
|
// Normalise CRLF → LF before validation so a Windows browser's
|
||||||
|
// submission scans byte-identical to the on-disk form.
|
||||||
|
$rawData = str_replace("\r", "", (string)$_POST['data']);
|
||||||
|
$cronBlockers = pistar_cron_validate($rawData);
|
||||||
|
|
||||||
|
if (!empty($cronBlockers)) {
|
||||||
|
// Validation failed — DO NOT touch /etc/crontab. Surface
|
||||||
|
// the diagnostics in the page and re-render the form
|
||||||
|
// with the operator's submitted content so they can
|
||||||
|
// fix in place without retyping.
|
||||||
|
$theData = $rawData;
|
||||||
|
error_log('Pi-Star fulledit_cron.php: rejected save with '
|
||||||
|
. count($cronBlockers) . ' blocked line(s)');
|
||||||
|
} else {
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU
|
||||||
|
// rationale. Per-request random staging path defeats
|
||||||
|
// the predictable-name pre-create / symlink class.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
$fh = fopen($filepath, 'w');
|
||||||
|
if ($fh !== false) {
|
||||||
|
fwrite($fh, $rawData);
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write through the one sudoers-allowlisted primitive for
|
||||||
|
// this file: `sudo sed -i … /etc/crontab`. (The previous
|
||||||
|
// `install … /etc/crontab` had no matching sudoers rule and
|
||||||
|
// was silently rejected — saves looked successful but never
|
||||||
|
// touched disk.) The operator's bytes are poured over the
|
||||||
|
// file with sed's `1r <file>` + `d` idiom: read the staged
|
||||||
|
// temp file in on the first cycle, delete every original
|
||||||
|
// line, leaving an exact byte-for-byte copy. The operator's
|
||||||
|
// content never enters the sed script — only the temp file
|
||||||
|
// PATH does — so there is no sed/shell-injection surface.
|
||||||
|
// sed -i preserves the existing root:root 644 (verified on a
|
||||||
|
// live host), so no follow-up chown/chmod is needed.
|
||||||
|
// $sedOut is an unused placeholder (sed -i is silent on
|
||||||
|
// success); the save status we act on is the exit code $rc
|
||||||
|
// plus the read-back comparison below.
|
||||||
|
$rc = 0;
|
||||||
|
$sedOut = array();
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo sed -i -e ' . escapeshellarg('1r ' . $filepath) . ' -e d /etc/crontab', $sedOut, $rc);
|
||||||
|
exec('sudo mount -o remount,ro /'); // always re-protect the rootfs
|
||||||
|
|
||||||
|
// Loud-failure check. The old code unconditionally rendered
|
||||||
|
// "as saved", which is exactly how the broken write hid for
|
||||||
|
// weeks. Read /etc/crontab back (world-readable) and compare
|
||||||
|
// byte-for-byte: a non-zero sed exit OR any mismatch means
|
||||||
|
// the save did not land. This also covers the degenerate
|
||||||
|
// empty-crontab corner, where `1r` never fires and the file
|
||||||
|
// would be left empty.
|
||||||
|
$onDisk = @file_get_contents('/etc/crontab');
|
||||||
|
if ($rc !== 0 || $onDisk !== $rawData) {
|
||||||
|
$saveError = true;
|
||||||
|
error_log('Pi-Star fulledit_cron.php: crontab save FAILED (sed rc='
|
||||||
|
. $rc . ', readback '
|
||||||
|
. ($onDisk === $rawData ? 'matched' : 'MISMATCHED') . ')');
|
||||||
|
} else {
|
||||||
|
$saveOk = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render the submitted content either way so a failed
|
||||||
|
// save never discards the operator's edits.
|
||||||
|
$theData = $rawData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/crontab ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
$fh = fopen($filepath, 'r');
|
||||||
|
if ($fh === false) {
|
||||||
|
// The privileged copy of /etc/crontab could not be staged
|
||||||
|
// (sudo cp/chown/chmod failed). Surface a read error and show
|
||||||
|
// an empty editor rather than a blank-but-editable textarea —
|
||||||
|
// saving from the latter would overwrite /etc/crontab with
|
||||||
|
// nothing.
|
||||||
|
$theData = '';
|
||||||
|
$readError = true;
|
||||||
|
error_log('Pi-Star fulledit_cron.php: could not stage /etc/crontab for reading');
|
||||||
|
} else {
|
||||||
|
$sz = filesize($filepath);
|
||||||
|
$theData = $sz > 0 ? fread($fh, $sz) : '';
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<?php if (!empty($cronBlockers)) { ?>
|
||||||
|
<div style="background-color: #ff9090; color: #f01010; padding: 10px; margin: 0 0 10px 0;">
|
||||||
|
<b>Save rejected — the cron editor blocks lines that match common privilege-escalation patterns.</b>
|
||||||
|
<ul style="margin: 8px 0 0 16px;">
|
||||||
|
<?php foreach ($cronBlockers as $b) {
|
||||||
|
echo '<li>' . htmlspecialchars($b, ENT_QUOTES, 'UTF-8') . "</li>\n";
|
||||||
|
} ?>
|
||||||
|
</ul>
|
||||||
|
<p style="margin: 8px 0 0 0;">Edit the offending line(s) and re-submit, or revert your changes.
|
||||||
|
If you have a legitimate need to schedule one of these patterns, use SSH and edit
|
||||||
|
<code>/etc/crontab</code> directly — the dashboard editor enforces these checks
|
||||||
|
to limit the damage of stolen dashboard credentials.</p>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if ($saveError) { ?>
|
||||||
|
<div style="background-color: #ff9090; color: #f01010; padding: 10px; margin: 0 0 10px 0;">
|
||||||
|
<b>Save failed — /etc/crontab was not updated.</b>
|
||||||
|
<p style="margin: 8px 0 0 0;">The write was rejected or the file did not match after saving. Your edits are
|
||||||
|
preserved below — try again, or edit <code>/etc/crontab</code> directly over SSH.</p>
|
||||||
|
</div>
|
||||||
|
<?php } elseif ($saveOk) { ?>
|
||||||
|
<div id="cronSaveOk" style="background-color: #c0f0c0; color: #106010; padding: 10px; margin: 0 0 10px 0;">
|
||||||
|
<b>Crontab saved.</b>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Auto-dismiss the success banner after 5s. The error banners are left in
|
||||||
|
// place on purpose so a failed save can never scroll away unnoticed.
|
||||||
|
setTimeout(function () {
|
||||||
|
var el = document.getElementById('cronSaveOk');
|
||||||
|
if (!el) { return; }
|
||||||
|
// Honour reduced-motion preferences: snap rather than fade.
|
||||||
|
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
el.style.transition = reduce ? 'none' : 'opacity 0.6s ease';
|
||||||
|
el.style.opacity = '0';
|
||||||
|
setTimeout(function () { if (el.parentNode) { el.parentNode.removeChild(el); } }, 650);
|
||||||
|
}, 5000);
|
||||||
|
</script>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if ($readError) { ?>
|
||||||
|
<div style="background-color: #ff9090; color: #f01010; padding: 10px; margin: 0 0 10px 0;">
|
||||||
|
<b>Could not read /etc/crontab.</b>
|
||||||
|
<p style="margin: 8px 0 0 0;">The current crontab could not be loaded, so the editor below is empty.
|
||||||
|
<b>Do not save</b> — doing so would overwrite /etc/crontab with nothing. Reload the page, or edit
|
||||||
|
<code>/etc/crontab</code> directly over SSH.</p>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<form name="test" method="post" action="">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<textarea name="data" cols="80" rows="45"><?php echo htmlspecialchars((string)$theData, ENT_QUOTES, 'UTF-8'); ?></textarea><br />
|
||||||
|
<input type="submit" name="submit" value="<?php echo $lang['apply']; ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/dapnetapi.key (DAPNET credentials).
|
||||||
|
*
|
||||||
|
* Same first-save-via-shell-echo pattern as fulledit_bmapikey.php.
|
||||||
|
* Read by dapnetgateway at startup; no daemon restart from this
|
||||||
|
* editor.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// /etc/dapnetapi.key holds DAPNET credentials, so the random-name
|
||||||
|
// TOCTOU defence is more important here than for the other editors.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
if (file_exists('/etc/dapnetapi.key')) {
|
||||||
|
exec('sudo cp /etc/dapnetapi.key ' . escapeshellarg($filepath));
|
||||||
|
} else {
|
||||||
|
// Seed the staging file with the empty-config skeleton.
|
||||||
|
// tempnam already created it owned by www-data, so PHP-side
|
||||||
|
// file_put_contents writes through directly.
|
||||||
|
file_put_contents(
|
||||||
|
$filepath,
|
||||||
|
"[DAPNETAPI]\nUSER=\nPASS=\nTRXAREA=\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
//after the form submit
|
||||||
|
if($_POST) {
|
||||||
|
$data = $_POST;
|
||||||
|
//update ini file, call function
|
||||||
|
update_ini_file($data, $filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is the function going to update your ini file
|
||||||
|
function update_ini_file($data, $filepath)
|
||||||
|
{
|
||||||
|
$content = "";
|
||||||
|
|
||||||
|
//parse the ini file to get the sections
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
foreach($data as $section=>$values) {
|
||||||
|
// UnBreak special cases
|
||||||
|
$section = str_replace("_", " ", $section);
|
||||||
|
$content .= "[".$section."]\n";
|
||||||
|
//append the values
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
// Strip CR/LF from values before they land in the INI
|
||||||
|
// file. The save handler writes `$key=$value\n` and a
|
||||||
|
// newline inside $value would split the value into a
|
||||||
|
// new INI line, allowing injection of arbitrary
|
||||||
|
// additional keys (e.g. `value=foo\nDEBUG=1`). On a
|
||||||
|
// single-operator device the practical risk is low
|
||||||
|
// but the sanitiser is one line.
|
||||||
|
$value = str_replace(array("\r", "\n"), "", (string)$value);
|
||||||
|
$content .= $key."=".$value."\n";
|
||||||
|
}
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
//write it into file
|
||||||
|
if (!$handle = fopen($filepath, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = fwrite($handle, $content);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Atomic install: mode + owner set in one syscall sequence.
|
||||||
|
// /etc/dapnetapi.key holds DAPNET credentials — mode 600 keeps
|
||||||
|
// them readable only by www-data. Owner left as www-data
|
||||||
|
// because the dapnetgateway daemon reads via the dashboard
|
||||||
|
// (and dashboard reads it directly without sudo elsewhere);
|
||||||
|
// see fulledit_bmapikey.php for the same rationale.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 600 -o www-data -g www-data '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/dapnetapi.key');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse the ini file using default parse_ini_file() PHP function
|
||||||
|
$parsed_ini = parse_ini_file($filepath, true);
|
||||||
|
|
||||||
|
echo '<form action="" method="post">'."\n";
|
||||||
|
echo csrf_field_html()."\n";
|
||||||
|
foreach($parsed_ini as $section=>$values) {
|
||||||
|
// Same hardening as edit_mmdvmhost.php (#23): escape every INI
|
||||||
|
// section / key / value before HTML interpolation. The save
|
||||||
|
// handler writes POST bytes verbatim so legitimate values
|
||||||
|
// (including any with `"` or `<`) round-trip byte-identically.
|
||||||
|
$sectionHtml = htmlspecialchars((string)$section, ENT_QUOTES, 'UTF-8');
|
||||||
|
// keep the section as hidden text so we can update once the form submitted
|
||||||
|
echo "<input type=\"hidden\" value=\"$sectionHtml\" name=\"$sectionHtml\" />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th colspan=\"2\">$sectionHtml</th></tr>\n";
|
||||||
|
// print all other values as input fields, so can edit.
|
||||||
|
// note the name='' attribute it has both section and key
|
||||||
|
foreach($values as $key=>$value) {
|
||||||
|
$keyHtml = htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$valueHtml = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
if (($key == "Options") || ($value)) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"$valueHtml\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
elseif (($key == "Display") && ($value == '')) {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"None\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "<tr><td align=\"right\" width=\"30%\">$keyHtml</td><td align=\"left\"><input type=\"text\" name=\"{$sectionHtml}[$keyHtml]\" value=\"0\" /></td></tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo '<input type="submit" value="'.$lang['apply'].'" />'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
echo "</form>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/dmrgateway.
|
||||||
|
*
|
||||||
|
* Companion to edit_dmrgateway.php — same target file, but this view
|
||||||
|
* exposes the entire INI as a single textarea so the operator can
|
||||||
|
* make edits the structured form doesn't cover. POST data is staged
|
||||||
|
* to /tmp/<obfuscated>.tmp then sudo-copied back to /etc/dmrgateway.
|
||||||
|
* Restarts BOTH mmdvmhost.service AND dmrgateway.service after save.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/dmrgateway ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
if(isset($_POST['data'])) {
|
||||||
|
// Write submitted data into the staging file.
|
||||||
|
$fh = fopen($filepath, 'w');
|
||||||
|
fwrite($fh, $_POST['data']);
|
||||||
|
fclose($fh);
|
||||||
|
// L-5: atomic install replaces the prior cp + chmod + chown
|
||||||
|
// triplet (rejected by the tightened sudoers — see
|
||||||
|
// edit_mmdvmhost.php for the full rationale).
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/dmrgateway');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart mmdvmhost.service'); // Reload MMDVMHost
|
||||||
|
exec('sudo systemctl restart dmrgateway.service'); // Reload DMRGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read for the form's textarea.
|
||||||
|
$fh = fopen($filepath, 'r');
|
||||||
|
$theData = fread($fh, filesize($filepath));
|
||||||
|
fclose($fh);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<form name="test" method="post" action="">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<textarea name="data" cols="80" rows="45"><?php echo htmlspecialchars((string)$theData, ENT_QUOTES, 'UTF-8'); ?></textarea><br />
|
||||||
|
<input type="submit" name="submit" value="<?php echo $lang['apply']; ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/pistar-remote.
|
||||||
|
*
|
||||||
|
* Pi-Star Remote-Control daemon config (DTMF-driven actions, command
|
||||||
|
* mappings, etc.). Saved via the standard staged-write pattern;
|
||||||
|
* daemon: pistar-remote.service.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// tempnam() up front so both POST and GET branches share the same
|
||||||
|
// per-request random staging path. Cleanup is registered once.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/pistar-remote ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
if(isset($_POST['data'])) {
|
||||||
|
// Write submitted data into the staging file.
|
||||||
|
$fh = fopen($filepath, 'w');
|
||||||
|
fwrite($fh, $_POST['data']);
|
||||||
|
fclose($fh);
|
||||||
|
// Atomic install: content + mode + owner set in one syscall
|
||||||
|
// sequence. /etc/pistar-remote is read by the pistar-remote
|
||||||
|
// service daemon and by dashboard pages via parse_ini_file
|
||||||
|
// (no sudo); 644 root:root keeps both working — same target
|
||||||
|
// as the bmapikey/dapnetapi B5 migration.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/pistar-remote');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
|
||||||
|
// Reload the affected daemon
|
||||||
|
exec('sudo systemctl restart pistar-remote.service'); // Reload the daemon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read the staging file for the form's textarea — in the POST
|
||||||
|
// branch this returns what the operator just submitted (and what's
|
||||||
|
// now in /etc); in the GET branch it returns the unmodified /etc
|
||||||
|
// content.
|
||||||
|
$fh = fopen($filepath, 'r');
|
||||||
|
$theData = fread($fh, filesize($filepath));
|
||||||
|
fclose($fh);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<form name="test" method="post" action="">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<textarea name="data" cols="80" rows="45"><?php echo htmlspecialchars((string)$theData, ENT_QUOTES, 'UTF-8'); ?></textarea><br />
|
||||||
|
<input type="submit" name="submit" value="<?php echo $lang['apply']; ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /usr/local/etc/RSSI.dat.
|
||||||
|
*
|
||||||
|
* The modem RSSI calibration table — non-/etc location, edited the
|
||||||
|
* same way as the other fulledit_* files. No daemon restart (MMDVMHost
|
||||||
|
* re-reads RSSI.dat on its own).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /usr/local/etc/RSSI.dat ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
if(isset($_POST['data'])) {
|
||||||
|
// Write submitted data into the staging file.
|
||||||
|
$fh = fopen($filepath, 'w');
|
||||||
|
fwrite($fh, $_POST['data']);
|
||||||
|
fclose($fh);
|
||||||
|
// Atomic install: /usr/local/etc/RSSI.dat is read by the
|
||||||
|
// MMDVMHost daemon (root) and is world-readable for the
|
||||||
|
// dashboard's editor round-trip; 644 root:root preserves both.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 644 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /usr/local/etc/RSSI.dat');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read for the form's textarea (POST: shows just-saved content;
|
||||||
|
// GET: shows current /etc content).
|
||||||
|
$fh = fopen($filepath, 'r');
|
||||||
|
$theData = fread($fh, filesize($filepath));
|
||||||
|
fclose($fh);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<form name="test" method="post" action="">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<textarea name="data" cols="80" rows="45"><?php echo htmlspecialchars((string)$theData, ENT_QUOTES, 'UTF-8'); ?></textarea><br />
|
||||||
|
<input type="submit" name="submit" value="<?php echo $lang['apply']; ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Raw text editor for /etc/wpa_supplicant/wpa_supplicant.conf.
|
||||||
|
*
|
||||||
|
* Operator-editable WiFi configuration. Standard staged-write pattern.
|
||||||
|
*
|
||||||
|
* No daemon restart from this file; the operator must reset the WiFi
|
||||||
|
* adapter (admin/wifi.php has buttons) for changes to take effect.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php
|
||||||
|
// A3-3 — see edit_ircddbgateway.php for the full TOCTOU rationale.
|
||||||
|
// wpa_supplicant.conf carries the WPA PSK in cleartext, so the
|
||||||
|
// random-name TOCTOU defence is more important here than for the
|
||||||
|
// other editors: a predictable name attack would let a local
|
||||||
|
// attacker pre-create /tmp/<known>.tmp as a symlink to a target
|
||||||
|
// they control reading and have our `sudo cp` follow it.
|
||||||
|
$filepath = tempnam('/tmp', 'pistar-edit-');
|
||||||
|
register_shutdown_function(function() use ($filepath) { @unlink($filepath); });
|
||||||
|
exec('sudo cp /etc/wpa_supplicant/wpa_supplicant.conf ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chown www-data:www-data ' . escapeshellarg($filepath));
|
||||||
|
exec('sudo chmod 600 ' . escapeshellarg($filepath));
|
||||||
|
|
||||||
|
if(isset($_POST['data'])) {
|
||||||
|
// Write submitted data into the staging file.
|
||||||
|
$fh = fopen($filepath, 'w');
|
||||||
|
fwrite($fh, $_POST['data']);
|
||||||
|
fclose($fh);
|
||||||
|
// Atomic install: mode + owner set in one syscall sequence.
|
||||||
|
// wpa_supplicant.conf carries the WPA PSK in cleartext as
|
||||||
|
// `psk=…hex…` — mode 600 root:root keeps every other local
|
||||||
|
// user (and any sandbox-escape from another service) from
|
||||||
|
// reading it. wpa_supplicant runs as root, so the daemon's
|
||||||
|
// own access is unaffected.
|
||||||
|
exec('sudo mount -o remount,rw /');
|
||||||
|
exec('sudo install -m 600 -o root -g root '
|
||||||
|
. escapeshellarg($filepath) . ' /etc/wpa_supplicant/wpa_supplicant.conf');
|
||||||
|
exec('sudo mount -o remount,ro /');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read for the form's textarea.
|
||||||
|
$fh = fopen($filepath, 'r');
|
||||||
|
$theData = fread($fh, filesize($filepath));
|
||||||
|
fclose($fh);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<form name="test" method="post" action="">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<textarea name="data" cols="80" rows="45"><?php echo htmlspecialchars((string)$theData, ENT_QUOTES, 'UTF-8'); ?></textarea><br />
|
||||||
|
<input type="submit" name="submit" value="<?php echo $lang['apply']; ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">CDN:<?php echo $configPistarRelease['Pi-Star']['Version']?> / 仪表盘:<?php echo $version; ?></div>
|
||||||
|
<h1>CDN 数字语音 - 专家编辑器</h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<b>快速编辑:</b>
|
||||||
|
<a href="edit_dstarrepeater.php" style="color: #ffffff;">DStarRepeater</a> |
|
||||||
|
<a href="edit_ircddbgateway.php" style="color: #ffffff;">ircDDBGateway</a> |
|
||||||
|
<a href="edit_timeserver.php" style="color: #ffffff;">TimeServer</a> |
|
||||||
|
<a href="edit_mmdvmhost.php" style="color: #ffffff;">MMDVMHost</a> |
|
||||||
|
<a href="edit_dmrgateway.php" style="color: #ffffff;">DMR 网关</a> |
|
||||||
|
<a href="edit_ysfgateway.php" style="color: #ffffff;">YSF 网关</a> |
|
||||||
|
<a href="edit_p25gateway.php" style="color: #ffffff;">P25 网关</a> |
|
||||||
|
<a href="edit_nxdngateway.php" style="color: #ffffff;">NXDN 网关</a> |
|
||||||
|
<a href="edit_dapnetgateway.php" style="color: #ffffff;">DAPNET 网关</a><br />
|
||||||
|
<b>完整编辑:</b>
|
||||||
|
<a href="fulledit_dmrgateway.php" style="color: #ffffff;">DMR 网关</a> |
|
||||||
|
<a href="fulledit_pistar-remote.php" style="color: #ffffff;">PiStar-Remote</a> |
|
||||||
|
<a href="fulledit_wpaconfig.php" style="color: #ffffff;">WiFi</a> |
|
||||||
|
<a href="fulledit_bmapikey.php" style="color: #ffffff;">BM API</a> |
|
||||||
|
<a href="fulledit_dapnetapi.php" style="color: #ffffff;">DAPNET API</a> |
|
||||||
|
<a href="fulledit_cron.php" style="color: #ffffff;">系统 Cron 任务</a> |
|
||||||
|
<a href="fulledit_rssidat.php" style="color: #ffffff;">RSSI 数据</a>
|
||||||
|
 <b>工具:</b>
|
||||||
|
<a href="modem_fw_upgrade.php" style="color: #ffffff;">固件</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Expert-section landing page.
|
||||||
|
*
|
||||||
|
* Renders the warning banner explaining what the expert editors do
|
||||||
|
* and that hand-edits can be overwritten by the dashboard. The
|
||||||
|
* navigation strip with links to every editor lives in
|
||||||
|
* header-menu.inc which is included from this file (and every
|
||||||
|
* editor page).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
//Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Expert Editor" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - Digital Voice Dashboard - Expert Editor</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr><th>Expert Editors</th></tr>
|
||||||
|
<tr><td align="center">
|
||||||
|
<h2>**WARNING**</h2>
|
||||||
|
Pi-Star Expert editors have been created to make editing some of the extra settings in the<br />
|
||||||
|
config files more simple, allowing you to update some areas of the config files without the<br />
|
||||||
|
need to login to your Pi over SSH.<br />
|
||||||
|
<br />
|
||||||
|
Please keep in mind when making your edits here, that these config files can be updated by<br />
|
||||||
|
the dashboard, and that your edits can be over-written. It is assumed that you already know<br />
|
||||||
|
what you are doing editing the files by hand, and that you understand what parts of the files<br />
|
||||||
|
are maintained by the dashboard.<br />
|
||||||
|
<br />
|
||||||
|
With that warning in mind, you are free to make any changes you like, for help come to the Facebook<br />
|
||||||
|
group (link at the bottom of the page) and ask for help if / when you need it.<br />
|
||||||
|
73 and enjoy your Pi-Star experiance.<br />
|
||||||
|
Pi-Star UK Team.<br />
|
||||||
|
<br />
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_new">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Network jitter test runner.
|
||||||
|
*
|
||||||
|
* Lets the operator pick a target (BrandMeister / DMR+ / HBLink) and
|
||||||
|
* runs `sudo /usr/local/sbin/pistar-jittertest <target>` on the
|
||||||
|
* device; output streams to /var/log/pi-star/pi-star_icmptest.log
|
||||||
|
* which this page tails via AJAX (jquery-timing $.repeat).
|
||||||
|
*
|
||||||
|
* Uses system()/exec() rather than the staged-write pattern — no
|
||||||
|
* file edits, just a privileged background process.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/expert/jitter_test.php") {
|
||||||
|
|
||||||
|
if (isset($_GET['group'])) {
|
||||||
|
if ($_GET['group'] == "brandmeister") { $target = "BM"; }
|
||||||
|
if ($_GET['group'] == "dmrplus") { $target = "DMR+"; }
|
||||||
|
if ($_GET['group'] == "hblink") { $target = "HB"; }
|
||||||
|
} else { $target = "DMR+"; }
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
// truncate creates+clears the log file in one synchronous call —
|
||||||
|
// see admin/update.php for the full rationale.
|
||||||
|
system('sudo truncate -s 0 /var/log/pi-star/pi-star_icmptest.log');
|
||||||
|
system('sudo /usr/local/sbin/pistar-jittertest '.$target.' > /dev/null 2>&1 &');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
//unset($_SESSION['update_offset']);
|
||||||
|
if (file_exists('/var/log/pi-star/pi-star_icmptest.log')) {
|
||||||
|
$_SESSION['update_offset'] = filesize('/var/log/pi-star/pi-star_icmptest.log');
|
||||||
|
} else {
|
||||||
|
$_SESSION['update_offset'] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
//session_start();
|
||||||
|
if (!file_exists('/var/log/pi-star/pi-star_icmptest.log')) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen('/var/log/pi-star/pi-star_icmptest.log', 'rb');
|
||||||
|
if (isset($_SESSION['update_offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['update_offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['update_offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['update_offset']);
|
||||||
|
$_SESSION['update_offset'] += strlen($data);
|
||||||
|
echo "<pre>$data</pre>";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['update_offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['update'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/expert/jitter_test.php?ajax', function(data) {
|
||||||
|
if (data.length < 1) return;
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<table width="100%">
|
||||||
|
<tr><th>Test Running</th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">Starting test, please wait...<br /></div></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Modem firmware upgrade runner.
|
||||||
|
*
|
||||||
|
* Calls `sudo /usr/local/sbin/pistar-modemupgrade list` to enumerate
|
||||||
|
* supported modem variants, then `sudo /usr/local/sbin/pistar-
|
||||||
|
* modemupgrade <variant>` to flash the selected one. Output streams
|
||||||
|
* to /var/log/pi-star/pi-star_modemflash.log; tailed via AJAX.
|
||||||
|
*
|
||||||
|
* The selected variant is run through escapeshellarg() before being
|
||||||
|
* passed to the upgrade script (defence-in-depth — the picklist is
|
||||||
|
* already constrained to script-reported names).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any state change (sed-i, fopen+fwrite, sudo cp, etc.).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
|
||||||
|
// Force the Locale to the stock locale just while we run the update
|
||||||
|
setlocale(LC_ALL, "LC_CTYPE=en_GB.UTF-8;LC_NUMERIC=C;LC_TIME=C;LC_COLLATE=C;LC_MONETARY=C;LC_MESSAGES=C;LC_PAPER=C;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=C;LC_IDENTIFICATION=C");
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/expert/modem_fw_upgrade.php") {
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['modem'])) {
|
||||||
|
// Whitelist-validate against the canonical modem list that
|
||||||
|
// pistar-modemupgrade itself reports. The script is the
|
||||||
|
// authoritative source of "what modem variants this device
|
||||||
|
// knows how to flash" — POSTed values that don't appear in
|
||||||
|
// that list are silently dropped (the operator just sees no
|
||||||
|
// flash happen). escapeshellarg() below is still defence-
|
||||||
|
// in-depth at the shell layer; this is the application-layer
|
||||||
|
// gate that stops the script from being invoked with
|
||||||
|
// attacker-controlled args at all. Mirrors the mmdvmcal
|
||||||
|
// command whitelist in admin/calibration.php.
|
||||||
|
$validModems = array_filter(array_map('trim',
|
||||||
|
explode("\n", (string)shell_exec('sudo /usr/local/sbin/pistar-modemupgrade list'))
|
||||||
|
));
|
||||||
|
if (in_array($_POST['modem'], $validModems, true)) {
|
||||||
|
$selectedOption = $_POST['modem'];
|
||||||
|
} else {
|
||||||
|
error_log('Pi-Star modem_fw_upgrade.php: rejected modem=' . substr((string)$_POST['modem'], 0, 64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
// truncate creates+clears the log file in one synchronous call —
|
||||||
|
// see admin/update.php for the full rationale.
|
||||||
|
system('sudo truncate -s 0 /var/log/pi-star/pi-star_modemflash.log');
|
||||||
|
// No `NP=1` env-var prefix: the path-scoped sudoers refuses
|
||||||
|
// env-var pass-through without a SETENV: tag, so `sudo NP=1
|
||||||
|
// pistar-modemupgrade …` was rejected outright with "you are
|
||||||
|
// not allowed to set the following environment variables: NP"
|
||||||
|
// and the flash silently never started. The wrapper now
|
||||||
|
// auto-detects non-interactive mode via `[ -t 0 ]` (it's
|
||||||
|
// backgrounded here with no controlling terminal), so the
|
||||||
|
// explicit env var is no longer needed.
|
||||||
|
if (isset($selectedOption)) { system('sudo /usr/local/sbin/pistar-modemupgrade ' . escapeshellarg($selectedOption) . ' > /dev/null 2>&1 &'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// passed sanity chk.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
// csrf_verify() at the top of the file already started the
|
||||||
|
// session via csrf_session_start(). Calling session_start() a
|
||||||
|
// second time emits a PHP Notice on PHP 8.x ("Ignoring
|
||||||
|
// session_start() because a session is already active") that
|
||||||
|
// gets logged to nginx error.log on every page load. The
|
||||||
|
// existing $_SESSION['update_offset'] log-tail logic below
|
||||||
|
// works unchanged against the already-active session.
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
if (file_exists('/var/log/pi-star/pi-star_modemflash.log')) {
|
||||||
|
$_SESSION['update_offset'] = filesize('/var/log/pi-star/pi-star_modemflash.log');
|
||||||
|
} else {
|
||||||
|
$_SESSION['update_offset'] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
if (!file_exists('/var/log/pi-star/pi-star_modemflash.log')) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$handle = fopen('/var/log/pi-star/pi-star_modemflash.log', 'rb');
|
||||||
|
if (isset($_SESSION['update_offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['update_offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['update_offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['update_offset']);
|
||||||
|
$_SESSION['update_offset'] += strlen($data);
|
||||||
|
echo nl2br($data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['update_offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the firmware version
|
||||||
|
if (file_exists('/usr/local/bin/firmware/version.txt')) {
|
||||||
|
$versionData = parse_ini_file('/usr/local/bin/firmware/version.txt', true);
|
||||||
|
}
|
||||||
|
if (isset($versionData['Firmware']['mmdvm_hs_version'])) {
|
||||||
|
$mmdvm_hs_version = $versionData['Firmware']['mmdvm_hs_version'];
|
||||||
|
$dvmega_fw_version = $versionData['Firmware']['dvmega_version'];
|
||||||
|
$rpt_version = $versionData['Firmware']['rpt_version'];
|
||||||
|
$fw_ver_msg = "Latest firmware version(s): <b> Hotspot:". $mmdvm_hs_version. " DV-Mega:".$dvmega_fw_version." Repeater:".$rpt_version."</b>.";
|
||||||
|
} else {
|
||||||
|
$fw_ver_msg = "Unkown (failed to retrieve firmware version).";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - Modem FW ".$lang['update'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function disableSubmitButtons()
|
||||||
|
{
|
||||||
|
var inputs = document.getElementsByTagName('input');
|
||||||
|
for (var i = 0; i < inputs.length; i++) {
|
||||||
|
if (inputs[i].type === 'button') {
|
||||||
|
inputs[i].disabled = true;
|
||||||
|
inputs[i].value = 'Please Wait...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitform()
|
||||||
|
{
|
||||||
|
disableSubmitButtons();
|
||||||
|
document.getElementById("up_fw").submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/expert/modem_fw_upgrade.php?ajax', function(data) {
|
||||||
|
if (data.length < 1) return;
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<table width="100%">
|
||||||
|
<?php if (empty($_POST['modem'])) { ?>
|
||||||
|
<tr><th>Modem Firmware Upgrade Utility</th></tr>
|
||||||
|
<tr><td>
|
||||||
|
<br />
|
||||||
|
<h2>Modem Firmware Upgrade Utility</h2>
|
||||||
|
<p>This tool will attempt to upgrade your selected modem to the latest version available firmware version:<br />
|
||||||
|
<?php echo $fw_ver_msg; ?></p>
|
||||||
|
<p>When ready, select your modem type below and click, "Upgrade Modem". Do not interrupt the process or<br />
|
||||||
|
navigate away from the page while the process is running.</p>
|
||||||
|
<p><strong>Please understand what you are doing, as well as the risks associated with flashing your modem.</strong></p>
|
||||||
|
<p><em>(IMPORTANT: Please note, we are not firmware developers, and we offer no support for firmware.<br />
|
||||||
|
We provide utilities to update the firmware. For firmware support, you will need to utilise other<br />
|
||||||
|
support resources from the firmware developers/maintainers or the web.)</em></p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$friendlyNames = [
|
||||||
|
'hs_hat' => 'MMDVM_HS_Hat (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_hat-12mhz' => 'MMDVM_HS_Hat (12.2880MHz TCXO) GPIO',
|
||||||
|
'hs_dual_hat' => 'MMDVM_HS_Dual_Hat (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_dual_hat-12mhz' => 'MMDVM_HS_Dual_Hat (12.2880MHz TCXO) GPIO',
|
||||||
|
'zum_rpi' => 'ZUMSpot RPi boards/hotspots GPIO',
|
||||||
|
'zum_rpi-duplex' => 'ZUMSpot RPi duplex GPIO board/hotspots',
|
||||||
|
'zum_usb' => 'ZUMspot USB stick',
|
||||||
|
'zum_libre' => 'ZUMspot Libre Kit or generic MMDVM_HS board',
|
||||||
|
'skybridge' => 'SkyBridge board/hotspots (14.7456MHz TCXO) GPIO',
|
||||||
|
'euronode' => 'DVMega-EuroNode hotspots (14.7456MHz TCXO) GPIO',
|
||||||
|
'nanodv_npi' => 'NANO_DV NPi GPIO by BG4TGO',
|
||||||
|
'nanodv_usb' => 'NANO_DV USB by BG4TG',
|
||||||
|
'hs_hat_ambe' => 'HS_HAT_AMBE (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_hat_lonestar-usb' => 'LoneStar LS MMDVM USB (14.7456MHz TCXO) USB',
|
||||||
|
'hs_hat_generic' => 'MMDVM_HS_GENERIC (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_hat_generic_duplex' => 'MMDVM_HS_GENERIC_DUPLEX (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_hat_generic_duplex-usb' => 'MMDVM_HS_GENERIC_DUPLEX (14.7456MHz TCXO) USB',
|
||||||
|
'hs_hat_nano_hotspot' => 'Nano_hotSPOT by BI7JTA (14.7456MHz TCXO) GPIO',
|
||||||
|
'hs_hat_nano_hotspot-duplex' => 'Nano_hotSPOT Dual by VR2VYE & BI7JTA (14.7456MHz TCXO) GPIO',
|
||||||
|
'dvmega_gpio' => 'DV-Mega Raspberry Pi Hat (Single or Dual Band) GPIO',
|
||||||
|
'dvmega_usb_uno' => 'DV-Mega Arduino Uno Shield USB (ttyUSB0)',
|
||||||
|
'dvmega_usb_mega' => 'DV-Mega Arduino Mega Shield USB (ttyUSB0)',
|
||||||
|
'rpt_nucleo-64' => 'Repeater - Nucleo64 F446RE GPIO',
|
||||||
|
'rpt_nucleo-144' => 'Repeater - Nucleo144 F767ZI GPIO',
|
||||||
|
'rpt_mmdvm_hat-0.2' => 'Repeater - MMDVM_RPT_Hat 0.2 GPIO',
|
||||||
|
'rpt_mmdvm_hat-0.3' => 'Repeater - MMDVM_RPT_Hat 0.3 GPIO',
|
||||||
|
'rpt_zum-0.9' => 'Repeater - ZUM Radio MMDVM for Pi v0.9 GPIO',
|
||||||
|
'rpt_zum-1.0' => 'Repeater - ZUM Radio MMDVM for Pi v1.0 GPIO',
|
||||||
|
'rpt_builder_v3' => 'Repeater - Repeater Builder STM32_DVM_PiHat V3 GPIO',
|
||||||
|
'rpt_builder_v4' => 'Repeater - Repeater Builder STM32_DVM_PiHat V4 GPIO',
|
||||||
|
'rpt_builder_v5' => 'Repeater - Repeater Builder STM32_DVM_PiHat V5 GPIO',
|
||||||
|
//'mmdvm_pi-f7' => 'MMDVM Pi F7 Board 460800 baud (12.000MHz TCXO) GPIO',
|
||||||
|
//'mmdvm_pi-f4' => 'MMDVM Pi F4 Board 460800 baud (12.000MHz TCXO) GPIO',
|
||||||
|
];
|
||||||
|
|
||||||
|
$output = shell_exec('sudo /usr/local/sbin/pistar-modemupgrade list');
|
||||||
|
|
||||||
|
if ($output !== null) {
|
||||||
|
// Split the output into an array of options
|
||||||
|
$options = explode("\n", trim($output));
|
||||||
|
|
||||||
|
// Create the select element
|
||||||
|
echo '<p><form method="post" id="up_fw">';
|
||||||
|
echo csrf_field_html();
|
||||||
|
echo '<label for="modem">Select Modem:</label>';
|
||||||
|
echo '<select id="modem" name="modem">';
|
||||||
|
echo '<option value="" disabled selected>Please choose device type...</option>';
|
||||||
|
// Output each option with user-friendly names
|
||||||
|
foreach ($options as $option) {
|
||||||
|
$friendlyName = isset($friendlyNames[$option]) ? $friendlyNames[$option] : $option;
|
||||||
|
echo '<option value="' . htmlspecialchars($option) . '">' . htmlspecialchars($friendlyName) . '</option>';
|
||||||
|
}
|
||||||
|
echo '</select>';
|
||||||
|
echo '<input type="button" value="Upgrade Modem" onclick="submitform()">';
|
||||||
|
echo '</form></p>';
|
||||||
|
} else {
|
||||||
|
echo '<p>Error executing the command.</p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Modem Flashing Tool © Chip Cuccio (W0CHP) 2020-<?php echo date("Y");?><br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php } else { ?>
|
||||||
|
|
||||||
|
<tr><th>Modem Flash/Upgrade Output</th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">Starting FW Upgrade, please wait...<br /></div></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Modem Flashing Tool © Chip Cuccio (W0CHP) 2020-<?php echo date("Y");?><br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php }
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ShellInABox iframe wrapper.
|
||||||
|
*
|
||||||
|
* Reads the configured shellinabox port from /etc/default/shellinabox
|
||||||
|
* and embeds the in-browser SSH terminal as an <iframe>. Uses
|
||||||
|
* setSecurityHeadersAllowDifferentPorts() to relax the default same-
|
||||||
|
* origin frame restriction so the iframe can target a non-80 port on
|
||||||
|
* the same host.
|
||||||
|
*
|
||||||
|
* No file edits, no privileged calls — pure UI wrapper.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeadersAllowDifferentPorts();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
|
||||||
|
if (file_exists('/etc/default/shellinabox')) {
|
||||||
|
$getPortCommand = "grep -m 1 'SHELLINABOX_PORT=' /etc/default/shellinabox | awk -F '=' '/SHELLINABOX_PORT=/ {print $2}'";
|
||||||
|
$shellPort = exec($getPortCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP_HOST is client-controllable (any HTTP client can send any
|
||||||
|
// `Host:` header). Echoing it raw into HTML attributes — as the
|
||||||
|
// iframe `src` and the anchor `href` below do — gives a passing
|
||||||
|
// attacker a reflected XSS surface: a request with a crafted Host
|
||||||
|
// header would render an iframe pointing at attacker-controlled
|
||||||
|
// content inside this page.
|
||||||
|
//
|
||||||
|
// Two-layer defence:
|
||||||
|
// 1. Strip everything that isn't a hostname character. The set
|
||||||
|
// `[a-zA-Z0-9.\-\[\]:]` covers DNS names, IPv4 dotted-quad,
|
||||||
|
// IPv6 bracketed literals, and the optional `:port` suffix.
|
||||||
|
// Anything else (CRLF, `<`, `"`, `'`, semicolons, spaces,
|
||||||
|
// parens, slashes) is dropped — the regex is what closes
|
||||||
|
// the XSS / response-splitting class.
|
||||||
|
// 2. htmlspecialchars on top, so even if the regex were
|
||||||
|
// relaxed in the future the value still can't break out
|
||||||
|
// of the `"…"` HTML attribute it lands in.
|
||||||
|
$shellHost = preg_replace(
|
||||||
|
'/[^a-zA-Z0-9.\-\[\]:]/',
|
||||||
|
'',
|
||||||
|
isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''
|
||||||
|
);
|
||||||
|
if ($shellHost === '') {
|
||||||
|
// Pathological case: HTTP_HOST contained nothing usable. Fall
|
||||||
|
// back to the LAN IP that nginx listens on rather than emitting
|
||||||
|
// a broken iframe `src=":port"`.
|
||||||
|
$shellHost = 'localhost';
|
||||||
|
}
|
||||||
|
$shellHostHtml = htmlspecialchars($shellHost, ENT_QUOTES, 'UTF-8');
|
||||||
|
$shellPortHtml = htmlspecialchars((string)(isset($shellPort) ? $shellPort : ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/expert/ssh_access.php") {
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - SSH";?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<table width="100%">
|
||||||
|
<tr><th>SSH - Pi-Star</th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">
|
||||||
|
<?php if (isset($shellPort)) {
|
||||||
|
echo "<iframe src=\"http://" . $shellHostHtml . ":" . $shellPortHtml . "\" style=\"border:0px #ffffff none; background:#ffffff; color:#00ff00;\" name=\"Pi-Star_SSH\" scrolling=\"no\" frameborder=\"0\" marginheight=\"0px\" marginwidth=\"0px\" height=\"100%\" width=\"100%\"></iframe>";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "SSH Feature not yet installed";
|
||||||
|
} ?>
|
||||||
|
</div></td></tr>
|
||||||
|
</table>
|
||||||
|
<?php if (isset($shellPort)) { echo "<a href=\"//" . $shellHostHtml . ":" . $shellPortHtml . "\">Click here for fullscreen SSH client</a><br />\n"; } ?>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pi-Star firmware/software upgrade runner.
|
||||||
|
*
|
||||||
|
* Triggers `sudo /usr/local/sbin/pistar-upgrade` (a longer-running
|
||||||
|
* sibling of /admin/update.php's pistar-update). Output streams to
|
||||||
|
* /var/log/pi-star/pi-star_upgrade.log and is tailed via AJAX.
|
||||||
|
* Requires an explicit POST confirmation field before kicking off.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Critical here: line 29 below kicks off `sudo pistar-upgrade`
|
||||||
|
// the moment a `confirm_update` POST lands. csrf_verify() MUST
|
||||||
|
// run before that, so a hostile cross-site POST can never start
|
||||||
|
// a long-running privileged upgrade on the device.
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('../config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('../config/version.php');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/expert/upgrade.php") {
|
||||||
|
|
||||||
|
// Only proceed with upgrade if user has confirmed via POST submission
|
||||||
|
if (!isset($_GET['ajax']) && !empty($_POST) && isset($_POST['confirm_update'])) {
|
||||||
|
// truncate creates+clears in one synchronous call — see
|
||||||
|
// admin/update.php for the full rationale (the prior touch + echo
|
||||||
|
// redirect pair was racy under `&` and the `>` ran as www-data,
|
||||||
|
// defeating the sudo on the truncation step).
|
||||||
|
system('sudo truncate -s 0 /var/log/pi-star/pi-star_upgrade.log');
|
||||||
|
system('sudo /usr/local/sbin/pistar-upgrade > /dev/null 2>&1 &');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
// session_start() is no longer called here — csrf_verify() at
|
||||||
|
// the top already started the session via csrf_session_start().
|
||||||
|
// The existing $_SESSION['update_offset'] log-tail logic below
|
||||||
|
// works unchanged against the already-active session.
|
||||||
|
|
||||||
|
// Initialize session offset only if upgrade has been confirmed
|
||||||
|
if (!isset($_GET['ajax']) && !empty($_POST) && isset($_POST['confirm_update'])) {
|
||||||
|
//unset($_SESSION['update_offset']);
|
||||||
|
if (file_exists('/var/log/pi-star/pi-star_upgrade.log')) {
|
||||||
|
$_SESSION['update_offset'] = filesize('/var/log/pi-star/pi-star_upgrade.log');
|
||||||
|
} else {
|
||||||
|
$_SESSION['update_offset'] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
//session_start();
|
||||||
|
if (!file_exists('/var/log/pi-star/pi-star_upgrade.log')) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen('/var/log/pi-star/pi-star_upgrade.log', 'rb');
|
||||||
|
if (isset($_SESSION['update_offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['update_offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['update_offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['update_offset']);
|
||||||
|
$_SESSION['update_offset'] += strlen($data);
|
||||||
|
echo nl2br($data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['update_offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['update'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||||
|
<?php if (!empty($_POST) && isset($_POST['confirm_update'])) { ?>
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/expert/upgrade.php?ajax', function(data) {
|
||||||
|
if (data.length < 1) return;
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php } ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<?php include './header-menu.inc'; ?>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php if (!empty($_POST) && isset($_POST['confirm_update'])) { ?>
|
||||||
|
<table role="presentation" width="100%">
|
||||||
|
<tr><th>Upgrade Running</th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">Starting upgrade, please wait...<br /></div></td></tr>
|
||||||
|
</table>
|
||||||
|
<?php } else { ?>
|
||||||
|
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<th>Upgrade</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 20px;">
|
||||||
|
<p style="margin-bottom: 20px;">
|
||||||
|
<strong>This will upgrade your Pi-Star installation to the latest version.</strong><br />
|
||||||
|
<br />
|
||||||
|
The upgrade process may take several minutes to complete.<br />
|
||||||
|
Please do not interrupt the process or power off your system during the upgrade.
|
||||||
|
</p>
|
||||||
|
<button style="border: none; background: none; cursor: pointer;" type="submit" name="confirm_update" value="1">
|
||||||
|
<img src="/images/download.png" border="0" alt="Start Upgrade" /><br />
|
||||||
|
<strong>Start Upgrade</strong>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<footer aria-label="footer">
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../images
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../index.php
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../lang/
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Live modem log viewer.
|
||||||
|
*
|
||||||
|
* AJAX-tails today's /var/log/pi-star/MMDVM-* (or DStarRepeater-*)
|
||||||
|
* log file in real time. Uses a session-stored byte offset so each
|
||||||
|
* `?ajax` call returns only the new bytes since the previous fetch,
|
||||||
|
* resetting if the log appears to have been rotated/truncated.
|
||||||
|
*
|
||||||
|
* Companion to download_modem_log.php (which sends the whole file
|
||||||
|
* in one go).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works. Affects
|
||||||
|
// both the wrapper-page render and the ?ajax tail endpoint; the latter
|
||||||
|
// is unreachable in practice unless the parent page already redirected.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/live_modem_log.php") {
|
||||||
|
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax'])) {
|
||||||
|
unset($_SESSION['offset']);
|
||||||
|
//$_SESSION['offset'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
//session_start();
|
||||||
|
if (file_exists('/etc/dstar-radio.mmdvmhost')) {
|
||||||
|
$logfile = "/var/log/pi-star/MMDVM-".gmdate('Y-m-d').".log";
|
||||||
|
}
|
||||||
|
elseif (file_exists('/etc/dstar-radio.dstarrepeater')) {
|
||||||
|
if (file_exists("/var/log/pi-star/DStarRepeater-".gmdate('Y-m-d').".log")) {$logfile = "/var/log/pi-star/DStarRepeater-".gmdate('Y-m-d').".log";}
|
||||||
|
if (file_exists("/var/log/pi-star/dstarrepeaterd-".gmdate('Y-m-d').".log")) {$logfile = "/var/log/pi-star/dstarrepeaterd-".gmdate('Y-m-d').".log";}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($logfile) || !file_exists($logfile)) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($logfile, 'rb');
|
||||||
|
if (isset($_SESSION['offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['offset']);
|
||||||
|
$_SESSION['offset'] += strlen($data);
|
||||||
|
echo nl2br($data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['live_logs'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/live_modem_log.php?ajax', function(data) {
|
||||||
|
if (data.length < 1) return;
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">Pi-Star:<?php echo $configPistarRelease['Pi-Star']['Version']?> / <?php echo $lang['dashboard'].": ".$version; ?></div>
|
||||||
|
<h1>Pi-Star <?php echo $lang['digital_voice']." - ".$lang['live_logs'];?></h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/power.php" style="color: #ffffff;"><?php echo $lang['power'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<table width="100%">
|
||||||
|
<tr><th><?php echo $lang['live_logs'];?></th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">Starting logging, please wait...<br /></div></td></tr>
|
||||||
|
<tr><th>Download the log: <a href="/admin/download_modem_log.php" style="color: #ffffff;">here</a></th></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../mmdvmhost/
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PiStar-Keeper logbook download.
|
||||||
|
*
|
||||||
|
* Sends /var/pistar-keeper/pistar-keeper.log as a binary download.
|
||||||
|
* Linked from /admin/admin.php's PiStar-Keeper Logbook panel
|
||||||
|
* (which only renders if pistar-keeper is running).
|
||||||
|
*
|
||||||
|
* NOTE for the security pass: no setSecurityHeaders() call, no auth
|
||||||
|
* check, no PHP_SELF guard. Coverage gap.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
$file = '/var/pistar-keeper/pistar-keeper.log';
|
||||||
|
|
||||||
|
if (file_exists($file)) {
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="'.basename($file). '"');
|
||||||
|
header('Content-Transfer-Encoding: binary');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate');
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Content-Length: ' . filesize($file));
|
||||||
|
flush();
|
||||||
|
readfile($file);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Reboot / shutdown control.
|
||||||
|
*
|
||||||
|
* Two POST actions:
|
||||||
|
* - reboot — `sudo sync` x3, remount-ro, then `sudo reboot &`.
|
||||||
|
* Renders a 90-second countdown that auto-redirects
|
||||||
|
* back to /index.php once the device is expected back.
|
||||||
|
* - shutdown — same sync/remount-ro, then `sudo shutdown -h now &`.
|
||||||
|
*
|
||||||
|
* The submit button on the form has a JS confirm() prompt to guard
|
||||||
|
* against accidental clicks. The PHP-level guard is just the
|
||||||
|
* PHP_SELF check at the top.
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Must run BEFORE any output: bootstraps the session on GET (so
|
||||||
|
// Set-Cookie ships) and rejects forged POSTs cleanly with 403
|
||||||
|
// before any side effect (`sudo reboot`, `sudo shutdown`).
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/power.php") {
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
// session_start() is no longer called here — csrf_verify() at
|
||||||
|
// the top of the file already started the session via
|
||||||
|
// csrf_session_start(). A second session_start() would emit a
|
||||||
|
// "session is already active" NOTICE on PHP 8.x.
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Power" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['power'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">Pi-Star:<?php echo $configPistarRelease['Pi-Star']['Version']?> / <?php echo $lang['dashboard'].": ".$version; ?></div>
|
||||||
|
<h1>Pi-Star <?php echo $lang['digital_voice']." - ".$lang['power'];?></h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/update.php" style="color: #ffffff;"><?php echo $lang['update'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php if (!empty($_POST)) {
|
||||||
|
// CSRF verification happens at the top of this file, before
|
||||||
|
// output begins. By the time execution reaches this block any
|
||||||
|
// forged POST has already been rejected.
|
||||||
|
?>
|
||||||
|
<table width="100%">
|
||||||
|
<tr><th colspan="2"><?php echo $lang['power'];?></th></tr>
|
||||||
|
<?php
|
||||||
|
if ( $_POST["action"] === "reboot" ) {
|
||||||
|
echo '<tr><td colspan="2" style="background: #000000; color: #00ff00;"><br /><br />Reboot command has been sent to your Pi,
|
||||||
|
<br />please wait up to 90 secs for it to reboot.<br />
|
||||||
|
<br />You will be re-directed back to the
|
||||||
|
<br />dashboard automatically in <span id="countdown">90</span> seconds.<br /><br /><br />
|
||||||
|
<script language="JavaScript" type="text/javascript">
|
||||||
|
var secondsLeft = 90;
|
||||||
|
var countdownElement = document.getElementById("countdown");
|
||||||
|
var countdownTimer = setInterval(function() {
|
||||||
|
secondsLeft--;
|
||||||
|
countdownElement.textContent = secondsLeft;
|
||||||
|
if (secondsLeft <= 0) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
setTimeout(function() { location.href = "/index.php"; }, 90000);
|
||||||
|
</script>
|
||||||
|
</td></tr>';
|
||||||
|
system('sudo sync && sudo sync && sudo sync && sudo mount -o remount,ro / > /dev/null &');
|
||||||
|
exec('sudo reboot > /dev/null &');
|
||||||
|
};
|
||||||
|
if ( $_POST["action"] === "shutdown" ) {
|
||||||
|
echo '<tr><td colspan="2" style="background: #000000; color: #00ff00;"><br /><br />Shutdown command has been sent to your Pi,
|
||||||
|
<br /> please wait 30 secs for it to fully shutdown<br />before removing the power.<br /><br /><br /></td></tr>';
|
||||||
|
system('sudo sync && sudo sync && sudo sync && sudo mount -o remount,ro / > /dev/null &');
|
||||||
|
exec('sudo shutdown -h now > /dev/null &');
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
</table>
|
||||||
|
<?php } else { ?>
|
||||||
|
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2"><?php echo $lang['power'];?></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
Reboot<br />
|
||||||
|
<button style="border: none; background: none;" name="action" value="reboot"><img src="/images/reboot.png" border="0" alt="Reboot" /></button>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
Shutdown<br />
|
||||||
|
<button style="border: none; background: none;" name="action" value="shutdown"><img src="/images/shutdown.png" border="0" alt="Shutdown" /></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* System info — RAM/swap, partition usage, and a long table of
|
||||||
|
* installed binary versions.
|
||||||
|
*
|
||||||
|
* Self-contained: no AJAX, no privileged calls. Reads /proc/meminfo,
|
||||||
|
* shells out to `df --block-size=1` to enumerate partitions, then
|
||||||
|
* runs each /usr/local/bin/<Gateway> with `-v` to print version
|
||||||
|
* banners (MMDVMHost, DMRGateway, DMR2YSF/NXDN, YSFGateway,
|
||||||
|
* DGIdGateway, YSF2DMR/P25/NXDN, P25Gateway, NXDNGateway,
|
||||||
|
* M17Gateway, DAPNETGateway).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
|
||||||
|
// Retrieve server information
|
||||||
|
$system = system_information();
|
||||||
|
|
||||||
|
function system_information()
|
||||||
|
{
|
||||||
|
@list($system, $host, $kernel) = preg_split('/[\s,]+/', php_uname('a'), 5);
|
||||||
|
$meminfo = false;
|
||||||
|
if (@is_readable('/proc/meminfo')) {
|
||||||
|
$data = explode("\n", file_get_contents("/proc/meminfo"));
|
||||||
|
$meminfo = array();
|
||||||
|
foreach ($data as $line) {
|
||||||
|
if (strpos($line, ':') !== false) {
|
||||||
|
list($key, $val) = explode(":", $line);
|
||||||
|
$meminfo[$key] = 1024 * floatval( trim( str_replace( ' kB', '', $val ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array('date' => date('Y-m-d H:i:s T'),
|
||||||
|
'mem_info' => $meminfo,
|
||||||
|
'partitions' => disk_list()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disk_list()
|
||||||
|
{
|
||||||
|
$partitions = array();
|
||||||
|
// Fetch partition information from df command
|
||||||
|
// I would have used disk_free_space() and disk_total_space() here but
|
||||||
|
// there appears to be no way to get a list of partitions in PHP?
|
||||||
|
$output = array();
|
||||||
|
@exec('df --block-size=1', $output);
|
||||||
|
foreach($output as $line) {
|
||||||
|
$columns = array();
|
||||||
|
foreach(explode(' ', $line) as $column) {
|
||||||
|
$column = trim($column);
|
||||||
|
if($column != '') $columns[] = $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process 6 column rows
|
||||||
|
// (This has the bonus of ignoring the first row which is 7)
|
||||||
|
if(count($columns) == 6) {
|
||||||
|
$partition = $columns[5];
|
||||||
|
$partitions[$partition]['Temporary']['bool'] = in_array($columns[0], array('tmpfs', 'devtmpfs'));
|
||||||
|
$partitions[$partition]['Partition']['text'] = $partition;
|
||||||
|
$partitions[$partition]['FileSystem']['text'] = $columns[0];
|
||||||
|
if(is_numeric($columns[1]) && is_numeric($columns[2]) && is_numeric($columns[3])) {
|
||||||
|
$partitions[$partition]['Size']['value'] = $columns[1];
|
||||||
|
$partitions[$partition]['Free']['value'] = $columns[3];
|
||||||
|
$partitions[$partition]['Used']['value'] = $columns[2];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fallback if we don't get numerical values
|
||||||
|
$partitions[$partition]['Size']['text'] = $columns[1];
|
||||||
|
$partitions[$partition]['Used']['text'] = $columns[2];
|
||||||
|
$partitions[$partition]['Free']['text'] = $columns[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $partitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize( $bytes )
|
||||||
|
{
|
||||||
|
$types = array( 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' );
|
||||||
|
for( $i = 0; $bytes >= 1024 && $i < ( count( $types ) -1 ); $bytes /= 1024, $i++ );
|
||||||
|
return( round( $bytes, 2 ) . " " . $types[$i] );
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star SysInfo" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['update'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<style>
|
||||||
|
.progress .bar + .bar {
|
||||||
|
-webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
-moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.progress-info .bar, .progress .bar-info {
|
||||||
|
background-color: #4bb1cf;
|
||||||
|
background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
|
||||||
|
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));
|
||||||
|
background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
|
||||||
|
background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
|
||||||
|
background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">Pi-Star:<?php echo $configPistarRelease['Pi-Star']['Version']?> / Dashboard:<?php echo $version; ?></div>
|
||||||
|
<h1>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - SysInfo";?></h1>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/power.php" style="color: #ffffff;"><?php echo $lang['power'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="contentwide">
|
||||||
|
<table role="presentation" width="100%" border="0">
|
||||||
|
<tr><th colspan="2">Pi-Star System Information</th></tr>
|
||||||
|
<?php
|
||||||
|
// Ram information
|
||||||
|
if ($system['mem_info']) {
|
||||||
|
echo " <tr><td><b>Memory</b></td><td><b>Stats</b></td></tr>\n";
|
||||||
|
$sysRamUsed = $system['mem_info']['MemTotal'] - $system['mem_info']['MemFree'] - $system['mem_info']['Buffers'] - $system['mem_info']['Cached'];
|
||||||
|
$sysRamPercent = sprintf('%.2f',($sysRamUsed / $system['mem_info']['MemTotal']) * 100);
|
||||||
|
echo " <tr><td align=\"left\">RAM</td><td align=\"left\"><div class='progress progress-info' style='margin-bottom: 0;'><div class='bar' style='width: ".$sysRamPercent."%;'>Used ".$sysRamPercent."%</div></div>";
|
||||||
|
echo " <b>Total:</b> ".formatSize($system['mem_info']['MemTotal'])."<b> Used:</b> ".formatSize($sysRamUsed)."<b> Free:</b> ".formatSize($system['mem_info']['MemTotal'] - $sysRamUsed)."</td></tr>\n";
|
||||||
|
}
|
||||||
|
// Filesystem Information
|
||||||
|
if (count($system['partitions']) > 0) {
|
||||||
|
echo " <tr><td><b>Mount</b></td><td><b>Stats</b></td></tr>\n";
|
||||||
|
foreach($system['partitions'] as $fs) {
|
||||||
|
if ($fs['Used']['value'] > 0 && $fs['FileSystem']['text']!= "none" && $fs['FileSystem']['text']!= "udev") {
|
||||||
|
$diskFree = $fs['Free']['value'];
|
||||||
|
$diskTotal = $fs['Size']['value'];
|
||||||
|
$diskUsed = $fs['Used']['value'];
|
||||||
|
$diskPercent = sprintf('%.2f',($diskUsed / $diskTotal) * 100);
|
||||||
|
|
||||||
|
echo " <tr><td align=\"left\">".$fs['Partition']['text']."</td><td align=\"left\"><div class='progress progress-info' style='margin-bottom: 0;'><div class='bar' style='width: ".$diskPercent."%;'>Used ".$diskPercent."%</div></div>";
|
||||||
|
echo " <b>Total:</b> ".formatSize($diskTotal)."<b> Used:</b> ".formatSize($diskUsed)."<b> Free:</b> ".formatSize($diskFree)."</td></tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Binary Information
|
||||||
|
echo " <tr><td><b>Binary</b></td><td><b>Version</b></td></tr>\n";
|
||||||
|
if (is_executable('/usr/local/bin/MMDVMHost')) {
|
||||||
|
$MMDVMHost_Ver = exec('/usr/local/bin/MMDVMHost -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">MMDVMHost</td><td align=\"left\">".$MMDVMHost_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/DMRGateway')) {
|
||||||
|
$DMRGateway_Ver = exec('/usr/local/bin/DMRGateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">DMRGateway</td><td align=\"left\">".$DMRGateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/DMR2YSF')) {
|
||||||
|
$DMR2YSF_Ver = exec('/usr/local/bin/DMR2YSF -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">DMR2YSF</td><td align=\"left\">".$DMR2YSF_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/DMR2NXDN')) {
|
||||||
|
$DMR2NXDN_Ver = exec('/usr/local/bin/DMR2NXDN -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">DMR2NXDN</td><td align=\"left\">".$DMR2NXDN_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/YSFGateway')) {
|
||||||
|
$YSFGateway_Ver = exec('/usr/local/bin/YSFGateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">YSFGateway</td><td align=\"left\">".$YSFGateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/DGIdGateway')) {
|
||||||
|
$DGIdGateway_Ver = exec('/usr/local/bin/DGIdGateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">DGIdGateway</td><td align=\"left\">".$DGIdGateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/YSF2DMR')) {
|
||||||
|
$YSF2DMR_Ver = exec('/usr/local/bin/YSF2DMR -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">YSF2DMR</td><td align=\"left\">".$YSF2DMR_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/YSF2P25')) {
|
||||||
|
$YSF2P25_Ver = exec('/usr/local/bin/YSF2P25 -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">YSF2P25</td><td align=\"left\">".$YSF2P25_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/YSF2NXDN')) {
|
||||||
|
$YSF2NXDN_Ver = exec('/usr/local/bin/YSF2NXDN -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">YSF2NXDN</td><td align=\"left\">".$YSF2NXDN_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/P25Gateway')) {
|
||||||
|
$P25Gateway_Ver = exec('/usr/local/bin/P25Gateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">P25Gateway</td><td align=\"left\">".$P25Gateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/NXDNGateway')) {
|
||||||
|
$NXDNGateway_Ver = exec('/usr/local/bin/NXDNGateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">NXDNGateway</td><td align=\"left\">".$NXDNGateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/M17Gateway')) {
|
||||||
|
$M17Gateway_Ver = exec('/usr/local/bin/M17Gateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">M17Gateway</td><td align=\"left\">".$M17Gateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
if (is_executable('/usr/local/bin/DAPNETGateway')) {
|
||||||
|
$DAPNETGateway_Ver = exec('/usr/local/bin/DAPNETGateway -v | cut -d\' \' -f 3-');
|
||||||
|
echo " <tr><td align=\"left\">DAPNETGateway</td><td align=\"left\">".$DAPNETGateway_Ver."</td></tr>\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pi-Star software update runner.
|
||||||
|
*
|
||||||
|
* On confirmed POST, kicks off `sudo /usr/local/sbin/pistar-update`
|
||||||
|
* in the background and tails /var/log/pi-star/pi-star_update.log
|
||||||
|
* via AJAX (jquery-timing $.repeat) — same pattern used by
|
||||||
|
* admin/expert/upgrade.php for pistar-upgrade and
|
||||||
|
* admin/expert/jitter_test.php for pistar-jittertest.
|
||||||
|
*
|
||||||
|
* Forces the locale to a stock C / en_GB.UTF-8 mix for the duration
|
||||||
|
* of the update so any commands the script runs see consistent
|
||||||
|
* formatting (some tools change their output based on locale).
|
||||||
|
*/
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/security_headers.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/csrf.php');
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'].'/config/banner_warnings.inc');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// CSRF protection — see config/csrf.php for the full rationale.
|
||||||
|
// Critical here: line 42 below kicks off `sudo pistar-update` the
|
||||||
|
// moment a `confirm_update` POST lands. csrf_verify() MUST run
|
||||||
|
// before that, so a hostile cross-site POST can never start a
|
||||||
|
// long-running privileged update on the device. The helper emits
|
||||||
|
// 403 + exit() on mismatch, well before the system() call.
|
||||||
|
csrf_verify();
|
||||||
|
|
||||||
|
// Layer 2 of the default-password protection — see config/banner_warnings.inc.
|
||||||
|
// MUST run BEFORE any output so header('Location: ...') works.
|
||||||
|
pistar_warnings_enforce_redirect();
|
||||||
|
|
||||||
|
// Load the language support
|
||||||
|
require_once('config/language.php');
|
||||||
|
// Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
// Load the Version Info
|
||||||
|
require_once('config/version.php');
|
||||||
|
|
||||||
|
// Force the Locale to the stock locale just while we run the update
|
||||||
|
setlocale(LC_ALL, "LC_CTYPE=en_GB.UTF-8;LC_NUMERIC=C;LC_TIME=C;LC_COLLATE=C;LC_MONETARY=C;LC_MESSAGES=C;LC_PAPER=C;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=C;LC_IDENTIFICATION=C");
|
||||||
|
|
||||||
|
// Sanity Check that this file has been opened correctly
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/update.php") {
|
||||||
|
|
||||||
|
// Only proceed with update if user has confirmed via POST submission
|
||||||
|
if (!isset($_GET['ajax']) && !empty($_POST) && isset($_POST['confirm_update'])) {
|
||||||
|
if (!file_exists('/var/log/pi-star')) {
|
||||||
|
system('sudo mkdir -p /var/log/pi-star/');
|
||||||
|
system('sudo chmod 775 /var/log/pi-star/');
|
||||||
|
system('sudo chown root:mmdvm /var/log/pi-star/');
|
||||||
|
}
|
||||||
|
// Truncate creates the file if missing and clears it if existing —
|
||||||
|
// does the work the prior `sudo touch` + `sudo echo "" > log` pair
|
||||||
|
// tried to do. The earlier echo was buggy (the `>` redirect was
|
||||||
|
// run as www-data, not under sudo, so it could only ever truncate
|
||||||
|
// a www-data-writable file — a silent no-op when the log was last
|
||||||
|
// touched as root). Run synchronously, BEFORE the backgrounded
|
||||||
|
// pistar-update spawn, so the script's first writes always land
|
||||||
|
// in a freshly cleared file regardless of `&` scheduling.
|
||||||
|
system('sudo truncate -s 0 /var/log/pi-star/pi-star_update.log');
|
||||||
|
system('sudo /usr/local/sbin/pistar-update > /dev/null 2>&1 &');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity Check Passed.
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
// session_start() is no longer called here — csrf_verify() at
|
||||||
|
// the top of the file already started the session via
|
||||||
|
// csrf_session_start(). A second session_start() would emit a
|
||||||
|
// "session is already active" NOTICE on PHP 8.x. The existing
|
||||||
|
// $_SESSION['update_offset'] log-tail logic below works
|
||||||
|
// unchanged against the already-active session.
|
||||||
|
|
||||||
|
// Initialize session offset only if update has been confirmed
|
||||||
|
if (!isset($_GET['ajax']) && !empty($_POST) && isset($_POST['confirm_update'])) {
|
||||||
|
//unset($_SESSION['update_offset']);
|
||||||
|
if (file_exists('/var/log/pi-star/pi-star_update.log')) {
|
||||||
|
$_SESSION['update_offset'] = filesize('/var/log/pi-star/pi-star_update.log');
|
||||||
|
} else {
|
||||||
|
$_SESSION['update_offset'] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
//session_start();
|
||||||
|
if (!file_exists('/var/log/pi-star/pi-star_update.log')) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen('/var/log/pi-star/pi-star_update.log', 'rb');
|
||||||
|
if (isset($_SESSION['update_offset'])) {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
if ($_SESSION['update_offset'] > ftell($handle)) //log rotated/truncated
|
||||||
|
$_SESSION['update_offset'] = 0; //continue at beginning of the new log
|
||||||
|
$data = stream_get_contents($handle, -1, $_SESSION['update_offset']);
|
||||||
|
$_SESSION['update_offset'] += strlen($data);
|
||||||
|
echo nl2br($data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fseek($handle, 0, SEEK_END);
|
||||||
|
$_SESSION['update_offset'] = ftell($handle);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="Pi-Star Update" />
|
||||||
|
<meta name="KeyWords" content="Pi-Star" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['update'];?></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
<?php if (!empty($_POST) && isset($_POST['confirm_update'])) { ?>
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/jquery-timing.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$.repeat(1000, function() {
|
||||||
|
$.get('/admin/update.php?ajax', function(data) {
|
||||||
|
if (data.length < 1) return;
|
||||||
|
var objDiv = document.getElementById("tail");
|
||||||
|
var isScrolledToBottom = objDiv.scrollHeight - objDiv.clientHeight <= objDiv.scrollTop + 1;
|
||||||
|
$('#tail').append(data);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php } ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php pistar_warnings_render(); ?>
|
||||||
|
<div class="container">
|
||||||
|
<header aria-label="header">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: right; padding-right: 8px;">
|
||||||
|
Pi-Star:<?php echo $configPistarRelease['Pi-Star']['Version']?> / Dashboard:<?php echo $version; ?>
|
||||||
|
</div>
|
||||||
|
<h1>Pi-Star - <?php echo $lang['digital_voice']." ".$lang['dashboard']." - ".$lang['update'];?></h1>
|
||||||
|
<nav aria-label="Menu">
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;"><?php echo $lang['admin'];?></a> |
|
||||||
|
<a href="/admin/power.php" style="color: #ffffff;"><?php echo $lang['power'];?></a> |
|
||||||
|
<a href="/admin/config_backup.php" style="color: #ffffff;"><?php echo $lang['backup_restore'];?></a> |
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="contentwide">
|
||||||
|
<?php if (!empty($_POST) && isset($_POST['confirm_update'])) { ?>
|
||||||
|
<table role="presentation" width="100%">
|
||||||
|
<tr><th>Update Running</th></tr>
|
||||||
|
<tr><td align="left"><div id="tail">Starting update, please wait...<br /></div></td></tr>
|
||||||
|
</table>
|
||||||
|
<?php } else { ?>
|
||||||
|
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<th><?php echo $lang['update'];?></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 20px;">
|
||||||
|
<p style="margin-bottom: 20px;">
|
||||||
|
<strong>This will update your Pi-Star installation to the latest version.</strong><br />
|
||||||
|
<br />
|
||||||
|
The update process may take several minutes to complete.<br />
|
||||||
|
Please do not interrupt the process or power off your system during the update.
|
||||||
|
</p>
|
||||||
|
<button style="border: none; background: none; cursor: pointer;" type="submit" name="confirm_update" value="1">
|
||||||
|
<img src="/images/download.png" border="0" alt="Start Update" /><br />
|
||||||
|
<strong>Start Update</strong>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer aria-label="Footer">
|
||||||
|
<div class="footer">
|
||||||
|
Pi-Star web config, © Andy Taylor (MW0MWZ) 2014-<?php echo date("Y"); ?>.<br />
|
||||||
|
Need help? Click <a style="color: #ffffff;" href="https://www.facebook.com/groups/pistarusergroup/" target="_new">here for the Support Group</a><br />
|
||||||
|
Get your copy of Pi-Star from <a style="color: #ffffff;" href="http://www.pistar.uk/downloads/" target="_blank">here</a>.<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
<?php
|
||||||
|
include('wifi/phpincs.php');
|
||||||
|
$output = $return = 0;
|
||||||
|
$page = $_GET['page'];
|
||||||
|
|
||||||
|
|
||||||
|
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="CDN Configuration" />
|
||||||
|
<meta name="KeyWords" content="CDN, MW0MWZ" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/pistar-css.php" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="wifi/styles.php" />
|
||||||
|
<script type="text/Javascript" src="wifi/functions.js?version=1.6"></script>
|
||||||
|
<title>CDN - 数字语音仪表盘 - WiFi 配置</title>
|
||||||
|
</head>
|
||||||
|
<body>'."\n";
|
||||||
|
switch($page) {
|
||||||
|
case "wlan0_info":
|
||||||
|
//Declare a pile of variables
|
||||||
|
$strIPAddress = NULL;
|
||||||
|
$strNetMask = NULL;
|
||||||
|
$strRxPackets = NULL;
|
||||||
|
$strRxBytes = NULL;
|
||||||
|
$strTxPackets = NULL;
|
||||||
|
$strTxBytes = NULL;
|
||||||
|
$strSSID = NULL;
|
||||||
|
$strBSSID = NULL;
|
||||||
|
$strBitrate = NULL;
|
||||||
|
$strTxPower = NULL;
|
||||||
|
$strLinkQuality = NULL;
|
||||||
|
$strSignalLevel = NULL;
|
||||||
|
$strWifiFreq = NULL;
|
||||||
|
$strWifiChan = NULL;
|
||||||
|
|
||||||
|
exec('ifconfig wlan0',$return);
|
||||||
|
exec('iwconfig wlan0',$return);
|
||||||
|
exec('iw dev wlan0 link',$return);
|
||||||
|
$strWlan0 = implode(" ",$return);
|
||||||
|
$strWlan0 = preg_replace('/\s\s+/', ' ', $strWlan0);
|
||||||
|
if (strpos($strWlan0,'HWaddr') !== false) {
|
||||||
|
preg_match('/HWaddr ([0-9a-f:]+)/i',$strWlan0,$result);
|
||||||
|
$strHWAddress = $result[1];
|
||||||
|
}
|
||||||
|
if (strpos($strWlan0,'ether') !== false) {
|
||||||
|
preg_match('/ether ([0-9a-f:]+)/i',$strWlan0,$result);
|
||||||
|
$strHWAddress = $result[1];
|
||||||
|
}
|
||||||
|
if(strpos($strWlan0, "UP") !== false && strpos($strWlan0, "RUNNING") !== false) {
|
||||||
|
$strStatus = '<span class="green">已启用</span>';
|
||||||
|
//Cant get these unless we are connected :)
|
||||||
|
if (strpos($strWlan0,'inet addr:') !== false) {
|
||||||
|
preg_match('/inet addr:([0-9.]+)/i',$strWlan0,$result);
|
||||||
|
$strIPAddress = $result[1];
|
||||||
|
} else {
|
||||||
|
preg_match('/inet ([0-9.]+)/i',$strWlan0,$result);
|
||||||
|
$strIPAddress = $result[1];
|
||||||
|
}
|
||||||
|
if (strpos($strWlan0,'Mask:') !== false) {
|
||||||
|
preg_match('/Mask:([0-9.]+)/i',$strWlan0,$result);
|
||||||
|
$strNetMask = $result[1];
|
||||||
|
} else {
|
||||||
|
preg_match('/netmask ([0-9.]+)/i',$strWlan0,$result);
|
||||||
|
$strNetMask = $result[1];
|
||||||
|
}
|
||||||
|
preg_match('/RX packets.(\d+)/',$strWlan0,$result);
|
||||||
|
$strRxPackets = $result[1];
|
||||||
|
preg_match('/TX packets.(\d+)/',$strWlan0,$result);
|
||||||
|
$strTxPackets = $result[1];
|
||||||
|
if (strpos($strWlan0,'RX bytes') !== false) {
|
||||||
|
preg_match('/RX [B|b]ytes:(\d+ \(\d+.\d+ [K|M|G]iB\))/i',$strWlan0,$result);
|
||||||
|
$strRxBytes = $result[1];
|
||||||
|
} else {
|
||||||
|
preg_match('/RX packets \d+ bytes (\d+ \(\d+.\d+ [K|M|G]iB\))/i',$strWlan0,$result);
|
||||||
|
$strRxBytes = $result[1];
|
||||||
|
}
|
||||||
|
if (strpos($strWlan0,'TX bytes') !== false) {
|
||||||
|
preg_match('/TX [B|b]ytes:(\d+ \(\d+.\d+ [K|M|G]iB\))/i',$strWlan0,$result);
|
||||||
|
$strTxBytes = $result[1];
|
||||||
|
} else {
|
||||||
|
preg_match('/TX packets \d+ bytes (\d+ \(\d+.\d+ [K|M|G]iB\))/i',$strWlan0,$result);
|
||||||
|
$strTxBytes = $result[1];
|
||||||
|
}
|
||||||
|
//preg_match('/TX Bytes:(\d+ \(\d+.\d+ [K|M|G]iB\))/i',$strWlan0,$result);
|
||||||
|
//$strTxBytes = $result[1];
|
||||||
|
if (preg_match('/Access Point: ([0-9a-f:]+)/i',$strWlan0,$result)) {
|
||||||
|
$strBSSID = $result[1]; }
|
||||||
|
if (preg_match('/Connected to\ ([0-9a-f:]+)/i',$strWlan0,$result)) {
|
||||||
|
$strBSSID = $result[1]; }
|
||||||
|
if (preg_match('/Bit Rate([=:0-9\.]+ Mb\/s)/i',$strWlan0,$result)) {
|
||||||
|
$strBitrate = str_replace(':', '', str_replace('=', '', $result[1])); }
|
||||||
|
if (preg_match('/tx bitrate:\ ([0-9\.]+ Mbit\/s)/i',$strWlan0,$result)) {
|
||||||
|
$strBitrate = str_replace(':', '', str_replace('=', '', $result[1])); }
|
||||||
|
if (preg_match('/Tx-Power=([0-9]+ dBm)/i',$strWlan0,$result)) {
|
||||||
|
$strTxPower = $result[1]; }
|
||||||
|
if (preg_match('/ESSID:\"([a-zA-Z0-9-_.\s]+)\"/i',$strWlan0,$result)) {
|
||||||
|
$strSSID = str_replace('"','',$result[1]); }
|
||||||
|
if (preg_match('/SSID:\ ([a-zA-Z0-9-_.\s]+)/i',$strWlan0,$result)) {
|
||||||
|
$strSSID = str_replace(' freq','',$result[1]); }
|
||||||
|
if (preg_match('/Link Quality=([0-9]+\/[0-9]+)/i',$strWlan0,$result)) {
|
||||||
|
$strLinkQuality = $result[1];
|
||||||
|
if (strpos($strLinkQuality, "/")) {
|
||||||
|
$arrLinkQuality = explode("/", $strLinkQuality);
|
||||||
|
$strLinkQuality = number_format(($arrLinkQuality[0] / $arrLinkQuality[1]) * 100)." %";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preg_match('/Signal Level=(-[0-9]+ dBm)/i',$strWlan0,$result)) {
|
||||||
|
$strSignalLevel = $result[1]; }
|
||||||
|
if (preg_match('/Signal Level=([0-9]+\/[0-9]+)/i',$strWlan0,$result)) {
|
||||||
|
$strSignalLevel = $result[1]; }
|
||||||
|
if (preg_match('/signal:\ (-[0-9]+ dBm)/i',$strWlan0,$result)) {
|
||||||
|
$strSignalLevel = $result[1]; }
|
||||||
|
if (preg_match('/Frequency:([0-9.]+ GHz)/i',$strWlan0,$result)) {
|
||||||
|
$strWifiFreq = $result[1];
|
||||||
|
$strWifiChan = str_replace(" GHz", "", $strWifiFreq);
|
||||||
|
$strWifiChan = str_replace(".", "", $strWifiChan);
|
||||||
|
$strWifiChan = ConvertToChannel(str_replace(".", "", $strWifiChan)); }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$strStatus = '<span class="red">未启用</span>';
|
||||||
|
}
|
||||||
|
if(isset($_POST['ifdown_wlan0'])) {
|
||||||
|
exec('ifconfig wlan0 | grep -i running | wc -l',$test);
|
||||||
|
if($test[0] == 1) {
|
||||||
|
exec('sudo ifdown wlan0',$return);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo '接口已经是关闭状态';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif(isset($_POST['ifup_wlan0'])) {
|
||||||
|
exec('ifconfig wlan0 | grep -i running | wc -l',$test);
|
||||||
|
if($test[0] == 0) {
|
||||||
|
exec('sudo ifup wlan0',$return);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo '接口已经是开启状态';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif(isset($_POST['reset_wlan0'])) {
|
||||||
|
exec('sudo wpa_cli reconfigure wlan0 && sudo ifdown wlan0 && sleep 3 && sudo ifup wlan0 && sudo wpa_cli scan',$test);
|
||||||
|
echo '<script>window.location.href=\'wifi.php?page=wlan0_info\';</script>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<script type="text/javascript">setTimeout(function () { location.reload(1); }, 15000);</script>
|
||||||
|
<div class="infobox">
|
||||||
|
<form action="'.$_SERVER['PHP_SELF'].'?page=wlan0_info" method="post">
|
||||||
|
<!-- <input type="submit" value="ifdown wlan0" name="ifdown_wlan0" /> -->
|
||||||
|
<!-- <input type="submit" value="ifup wlan0" name="ifup_wlan0" /> -->
|
||||||
|
<!-- <input type="button" value="Refresh" onclick="document.location.reload(true)" /> -->
|
||||||
|
<input type="button" value="刷新" onclick="window.location.href=\'wifi.php?page=wlan0_info\'" />
|
||||||
|
<input type="submit" value="重置 WiFi 适配器" name="reset wlan0" />
|
||||||
|
<input type="button" value="配置 WiFi" name="wpa_conf" onclick="document.location=\'?page=\'+this.name" />
|
||||||
|
</form>
|
||||||
|
<div class="infoheader">无线信息与统计</div>
|
||||||
|
<div class="intinfo"><div class="intheader">接口信息</div>
|
||||||
|
接口名称 : wlan0<br />
|
||||||
|
接口状态 : ' . $strStatus . '<br />
|
||||||
|
IP 地址 : ' . $strIPAddress . '<br />
|
||||||
|
子网掩码 : ' . $strNetMask . '<br />
|
||||||
|
MAC 地址 : ' . $strHWAddress . '<br />
|
||||||
|
<br />
|
||||||
|
<div class="intheader">接口统计</div>
|
||||||
|
接收数据包 : ' . $strRxPackets . '<br />
|
||||||
|
接收字节数 : ' . $strRxBytes . '<br />
|
||||||
|
发送数据包 : ' . $strTxPackets . '<br />
|
||||||
|
发送字节数 : ' . $strTxBytes . '<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="wifiinfo">
|
||||||
|
<div class="intheader">无线信息</div>
|
||||||
|
已连接到 : ' . $strSSID . '<br />
|
||||||
|
接入点 MAC 地址 : ' . $strBSSID . '<br />
|
||||||
|
<br />
|
||||||
|
传输速率 : ' . $strBitrate . '<br />
|
||||||
|
信号强度 : ' . $strSignalLevel . '<br />
|
||||||
|
<br />';
|
||||||
|
if ($strTxPower) { echo ' 发射功率 : ' . $strTxPower .'<br />'."\n"; } else { echo "<br />\n"; }
|
||||||
|
if ($strLinkQuality) { echo ' 链路质量 : ' . $strLinkQuality . '<br />'."\n"; } else { echo "<br />\n"; }
|
||||||
|
if (($strWifiFreq) && ($strWifiChan) && ($strWifiChan != "Invalid Channel")) {
|
||||||
|
echo ' 信道信息 : ' . $strWifiChan . ' (' . $strWifiFreq . ')<br />'."\n";
|
||||||
|
} else {
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
if (file_exists('/etc/wpa_supplicant/wpa_supplicant.conf')) {
|
||||||
|
exec('sudo grep "country" /etc/wpa_supplicant/wpa_supplicant.conf', $wifiCountryArr);
|
||||||
|
}
|
||||||
|
if (isset($wifiCountryArr[0])) {
|
||||||
|
$wifiCountry = explode("=", $wifiCountryArr[0]);
|
||||||
|
if (isset($wifiCountry[1])) {
|
||||||
|
echo ' WiFi 国家代码 : '.$wifiCountry[1]."<br />\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '<br />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="intfooter">信息来源:ifconfig 和 iwconfig</div>';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "wpa_conf":
|
||||||
|
exec('sudo cat /etc/wpa_supplicant/wpa_supplicant.conf',$return);
|
||||||
|
$ssid = array();
|
||||||
|
$psk = array();
|
||||||
|
foreach($return as $a) {
|
||||||
|
if(preg_match('/country=/i',$a)) {
|
||||||
|
$wifiCountryArr = explode("=",$a);
|
||||||
|
$wifiCountry = $wifiCountryArr[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we only put ONE SSID and matching PSK into the arrays
|
||||||
|
if ( ( isset($curssidplain) || isset($curssidalt) ) && ( isset($curpskplain) || isset($curpskalt) ) ) {
|
||||||
|
if (isset($curssidplain)) { $ssid[] = $curssidplain; unset($curssidplain); unset($curssidalt); }
|
||||||
|
if (isset($curssidalt)) { $ssid[] = $curssidalt; unset($curssidplain); unset($curssidalt); }
|
||||||
|
if (isset($curpskplain)) { $psk[] = $curpskplain; unset($curpskplain); unset($curpskalt); }
|
||||||
|
if (isset($curpskalt)) { $psk[] = $curpskalt; unset($curpskplain); unset($curpskalt); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case of the old file format, and the new...
|
||||||
|
if(preg_match('/\#SSID=/i',$a) && !preg_match('/scan_ssid/i',$a)) {
|
||||||
|
$arrssid = explode("=",$a);
|
||||||
|
//$ssid[] = str_replace('"','',$arrssid[1]);
|
||||||
|
$curssidplain = str_replace('"','',$arrssid[1]);
|
||||||
|
}
|
||||||
|
elseif(preg_match('/SSID="/i',$a) && !preg_match('/scan_ssid/i',$a)) {
|
||||||
|
$arrssid = explode("=",$a);
|
||||||
|
//$ssid[] = str_replace('"','',$arrssid[1]);
|
||||||
|
if (!isset($curssidplain)) { $curssidalt = str_replace('"','',$arrssid[1]); }
|
||||||
|
}
|
||||||
|
if (isset($curssidplain) || isset($curssidalt)) {
|
||||||
|
if(preg_match('/\#psk="/i',$a)) {
|
||||||
|
$arrpsk = explode("=",$a);
|
||||||
|
//$psk[] = str_replace('"','',$arrpsk[1]);
|
||||||
|
$curpskplain = str_replace('"','',$arrpsk[1]);
|
||||||
|
}
|
||||||
|
elseif(preg_match('/psk=/i',$a)) {
|
||||||
|
$arrpsk = explode("=",$a);
|
||||||
|
//$psk[] = str_replace('"','',$arrpsk[1]);
|
||||||
|
if (!isset($curpskplain)) { $curpskalt = str_replace('"','',$arrpsk[1]); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$numSSIDs = count($ssid);
|
||||||
|
$output = '<form method="post" action="'.$_SERVER['PHP_SELF'].'?page=wpa_conf" id="wpa_conf_form">
|
||||||
|
<input type="button" value="WiFi 信息" name="wlan0_info" onclick="document.location=\'?page=\'+this.name" /><br />
|
||||||
|
<input type="hidden" id="Networks" name="Networks" />
|
||||||
|
<div class="network" id="networkbox">'."\n";
|
||||||
|
if (!isset($wifiCountry)) { $wifiCountry = "JP"; }
|
||||||
|
$output .= 'WiFi 法规域(国家代码): <select name="wifiCountryCode">'."\n";
|
||||||
|
if (file_exists('/lib/crda/regulatory.bin')) {
|
||||||
|
exec('regdbdump /lib/crda/regulatory.bin | fgrep country | cut -b 9-10', $regDomains);
|
||||||
|
} elseif (file_exists('/lib/crda/db.txt')) {
|
||||||
|
exec('cat /lib/crda/db.txt | fgrep country | cut -b 9-10', $regDomains);
|
||||||
|
} else {
|
||||||
|
$regDomains = array("AU","FR","DE","GB","US","JP");
|
||||||
|
}
|
||||||
|
foreach($regDomains as $regDomain) {
|
||||||
|
if ($regDomain == $wifiCountry) {
|
||||||
|
$output .= '<option value="'.$regDomain.'" selected>'.$regDomain.'</option>'."\n";
|
||||||
|
} else {
|
||||||
|
$output .= '<option value="'.$regDomain.'">'.$regDomain.'</option>'."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output .= '</select><br />'."\n";
|
||||||
|
|
||||||
|
for($ssids = 0; $ssids < $numSSIDs; $ssids++) {
|
||||||
|
$output .= '<div id="Networkbox'.$ssids.'" class="NetworkBoxes">网络 '.$ssids."\n";
|
||||||
|
$output .= '<input type="button" value="删除" onclick="DeleteNetwork('.$ssids.')" /><br />'."\n";
|
||||||
|
$output .= '<span class="tableft" id="lssid'.$ssids.'">SSID :</span><input type="text" id="ssid'.$ssids.'" name="ssid'.$ssids.'" value="'.$ssid[$ssids].'" onkeyup="CheckSSID(this)" /><br />'."\n";
|
||||||
|
$output .= '<span class="tableft" id="lpsk'.$ssids.'">PSK :</span><input type="password" id="psk'.$ssids.'" name="psk'.$ssids.'" value="'.$psk[$ssids].'" onkeyup="CheckPSK(this)" /><br /><br /></div>'."\n";
|
||||||
|
}
|
||||||
|
$output .= '</div>'."\n";
|
||||||
|
$output .= '<div class="infobox">'."\n";
|
||||||
|
$output .= '<input type="submit" value="扫描网络(10 秒)" name="Scan" />'."\n";
|
||||||
|
$output .= '<input type="button" value="添加网络" onclick="AddNetwork();" />'."\n";
|
||||||
|
$output .= '<input type="submit" value="保存并连接" name="SaveWPAPSKSettings" onmouseover="UpdateNetworks(this)" />'."\n";
|
||||||
|
$output .= '</div>'."\n";
|
||||||
|
$output .= '</form>'."\n";
|
||||||
|
|
||||||
|
|
||||||
|
echo $output;
|
||||||
|
echo '<script type="text/Javascript">UpdateNetworks()</script>';
|
||||||
|
|
||||||
|
if(isset($_POST['SaveWPAPSKSettings'])) {
|
||||||
|
$config = "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\nupdate_config=1\nap_scan=1\nfast_reauth=1\ncountry=".$_POST['wifiCountryCode']."\n\n";
|
||||||
|
$networks = $_POST['Networks'];
|
||||||
|
|
||||||
|
//Reworked WiFi Starts Here
|
||||||
|
for($x = 0; $x < $networks; $x++) {
|
||||||
|
//$network = '';
|
||||||
|
$ssid = $_POST['ssid'.$x];
|
||||||
|
$psk = $_POST['psk'.$x];
|
||||||
|
$priority = 100 - $x;
|
||||||
|
if ($ssid == "*" && !$psk) { $config .= "network={\n\t#ssid=\"$ssid\"\n\t#psk=\"\"\n\tkey_mgmt=NONE\n\tid_str=\"$x\"\n\tpriority=$priority\n\tscan_ssid=1\n}\n\n"; }
|
||||||
|
elseif ($ssid && !$psk) { $config .= "network={\n\tssid=\"$ssid\"\n\t#psk=\"\"\n\tkey_mgmt=NONE\n\tid_str=\"$x\"\n\tpriority=$priority\n\tscan_ssid=1\n}\n\n"; }
|
||||||
|
elseif ($ssid && $psk) {
|
||||||
|
$pskSalted = hash_pbkdf2("sha1",$psk, $ssid, 4096, 64);
|
||||||
|
$ssidHex = bin2hex("$ssid");
|
||||||
|
$config .= "network={\n\t#ssid=\"$ssid\"\n\tssid=$ssidHex\n\t#psk=\"$psk\"\n\tpsk=$pskSalted\n\tid_str=\"$x\"\n\tpriority=$priority\n\tscan_ssid=1\n}\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_put_contents('/tmp/wifidata', $config);
|
||||||
|
system('sudo mount -o remount,rw / && sudo cp -f /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant.conf && sudo sync && sudo sync && sudo sync && sudo mount -o remount,ro /');
|
||||||
|
echo "WiFi 设置已成功更新\n";
|
||||||
|
// If Auto AP is on, dont restart the WiFi Card
|
||||||
|
if (!file_exists('/sys/class/net/wlan0_ap')) {
|
||||||
|
exec('sudo ip link set wlan0 down && sleep 3 && sudo ip link set wlan0 up');
|
||||||
|
}
|
||||||
|
echo "<script>document.location='?page=\wlan0_info'</script>";
|
||||||
|
|
||||||
|
} elseif(isset($_POST['Scan'])) {
|
||||||
|
$return = '';
|
||||||
|
exec('ifconfig wlan0 | grep -i running | wc -l',$test);
|
||||||
|
exec('sudo wpa_cli scan -i wlan0',$return);
|
||||||
|
sleep(8);
|
||||||
|
exec('sudo wpa_cli scan_results -i wlan0',$return);
|
||||||
|
unset($return['0']); // This is a better way to clean up;
|
||||||
|
unset($return['1']); // This is a better way to clean up;
|
||||||
|
echo "<br />\n";
|
||||||
|
echo "<div class=\"network\">\n";
|
||||||
|
echo "发现的网络 : <br />\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo "<tr><th>连接</th><th>SSID</th><th>信道</th><th>信号</th><th>加密方式</th></tr>";
|
||||||
|
foreach($return as $network) {
|
||||||
|
$arrNetwork = preg_split("/[\t]+/",$network);
|
||||||
|
$bssid = $arrNetwork[0];
|
||||||
|
$channel = ConvertToChannel($arrNetwork[1]);
|
||||||
|
$signal = $arrNetwork[2] . " dBm";
|
||||||
|
$security = ConvertToSecurity($arrNetwork[3]);
|
||||||
|
$ssid = $arrNetwork[4];
|
||||||
|
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<td style="text-align: left;"><input type="button" value="选择" onclick="AddScanned(\''.$ssid.'\')" /></td>';
|
||||||
|
echo '<td style="text-align: left;">'.$ssid.'</td>';
|
||||||
|
echo '<td style="text-align: left;">'.$channel.'</td>';
|
||||||
|
echo '<td>'.$signal.'</td>';
|
||||||
|
echo '<td style="text-align: left;">'.$security.'</td>';
|
||||||
|
echo '</tr>'."\n";
|
||||||
|
|
||||||
|
}
|
||||||
|
echo "</table>\n";
|
||||||
|
echo "</div>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<div class="tail">.</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
?>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
function WiFiDown() {
|
||||||
|
var down = confirm("确定要关闭 wlan0 吗?");
|
||||||
|
if(down) {
|
||||||
|
} else {
|
||||||
|
alert("操作已取消");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateNetworks() {
|
||||||
|
var existing = document.getElementById("networkbox").getElementsByTagName('div').length;
|
||||||
|
document.getElementById("Networks").value = existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNetwork() {
|
||||||
|
var Networks = document.getElementById('Networks').value;
|
||||||
|
document.getElementById('networkbox').innerHTML += '<div id="Networkbox'+Networks+'" class="Networkboxes">网络 '+Networks+'<input type="button" value="删除" onClick="DeleteNetwork('+Networks+')" /><br /> \
|
||||||
|
<span class="tableft">SSID :</span><input type="text" name="ssid'+Networks+'" onkeyup="CheckSSID(this)"><br /> \
|
||||||
|
<span class="tableft">PSK :</span><input type="password" name="psk'+Networks+'" onkeyup="CheckPSK(this)"></div>';
|
||||||
|
Networks++;
|
||||||
|
document.getElementById('Networks').value=Networks;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddScanned(network) {
|
||||||
|
existing = document.getElementById("networkbox").getElementsByTagName('div').length;
|
||||||
|
var Networks = document.getElementById('Networks').value;
|
||||||
|
if(existing != 0) {
|
||||||
|
document.getElementById('Networks').value = Networks;
|
||||||
|
}
|
||||||
|
//document.getElementById('Networks').value=Networks;
|
||||||
|
document.getElementById('networkbox').innerHTML += '<div id="Networkbox'+Networks+'" class="Networkboxes">网络 '+Networks+'<input type="button" value="删除" /><br /> \
|
||||||
|
<span class="tableft">SSID :</span><input type="text" name="ssid'+Networks+'" id="ssid'+Networks+'" onkeyup="CheckSSID(this)"><br /> \
|
||||||
|
<span class="tableft">PSK :</span><input type="password" name="psk'+Networks+'" onkeyup="CheckPSK(this)"></div>';
|
||||||
|
document.getElementById('ssid'+Networks).value = network;
|
||||||
|
if(existing == 0) {
|
||||||
|
Networks++
|
||||||
|
document.getElementById('Networks').value = Networks;
|
||||||
|
}
|
||||||
|
document.getElementById('Networks').value = Networks;
|
||||||
|
Networks++
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckSSID(ssid) {
|
||||||
|
if(ssid.value.length>31) {
|
||||||
|
ssid.style.background='#FFD0D0';
|
||||||
|
document.getElementById('Save').disabled = true;
|
||||||
|
} else {
|
||||||
|
ssid.style.background='#D0FFD0';
|
||||||
|
document.getElementById('Save').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckPSK(psk) {
|
||||||
|
if(psk.value.length > 0 && psk.value.length < 8) {
|
||||||
|
psk.style.background='#FFD0D0';
|
||||||
|
document.getElementById('Save').disabled = true;
|
||||||
|
} else {
|
||||||
|
psk.style.background='#D0FFD0';
|
||||||
|
document.getElementById('Save').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteNetwork(network) {
|
||||||
|
element = document.getElementById('Networkbox'+network);
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
var Networks = document.getElementById('Networks').value;
|
||||||
|
Networks--
|
||||||
|
document.getElementById('Networks').value = Networks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
// Empty placeholder: presence prevents directory listing under Apache
|
||||||
|
// when no DirectoryIndex match is found in this folder.
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Helper functions for the WiFi configuration page (admin/wifi.php).
|
||||||
|
*
|
||||||
|
* Three small utilities:
|
||||||
|
* - {@see GetDistString()} Substring extraction by offset + delimiter.
|
||||||
|
* - {@see ParseConfig()} Convert an array of `key=value` lines into
|
||||||
|
* an associative array, skipping comments.
|
||||||
|
* - {@see ConvertToChannel()} Map a wireless centre frequency (MHz, as
|
||||||
|
* reported by `iwconfig` / `iw dev`) to a
|
||||||
|
* human-readable "2.4GHz Ch6" / "5.0GHz
|
||||||
|
* Ch149" label.
|
||||||
|
* - {@see ConvertToSecurity()} Map a wpa_supplicant security flag string
|
||||||
|
* to a friendly label (e.g. WPA2-PSK (AES)).
|
||||||
|
*
|
||||||
|
* Included from admin/wifi.php to back the wireless info / scan UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a substring from `$input` starting `$offset` bytes after the
|
||||||
|
* occurrence of `$string`, ending at the first `$separator` after that.
|
||||||
|
*
|
||||||
|
* @param string $input The haystack to search.
|
||||||
|
* @param string $string Anchor token that marks the start point.
|
||||||
|
* @param int $offset Byte offset added past the anchor.
|
||||||
|
* @param string $separator Delimiter that ends the slice.
|
||||||
|
* @return string The extracted substring.
|
||||||
|
*/
|
||||||
|
function GetDistString($input, $string, $offset, $separator)
|
||||||
|
{
|
||||||
|
$string = substr($input, strpos($input, $string) + $offset, strpos(substr($input, strpos($input, $string) + $offset), $separator));
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an array of `key=value` lines (typically from a wpa_supplicant
|
||||||
|
* or hostapd config) into an associative array. Lines beginning with
|
||||||
|
* `#` are treated as comments and skipped.
|
||||||
|
*
|
||||||
|
* @param array<int,string> $arrConfig One line per element.
|
||||||
|
* @return array<string,string> Keyed by the LHS of the first `=` per line.
|
||||||
|
*/
|
||||||
|
function ParseConfig($arrConfig)
|
||||||
|
{
|
||||||
|
$config = array();
|
||||||
|
foreach ($arrConfig as $line) {
|
||||||
|
if ($line[0] != "#") {
|
||||||
|
$arrLine = explode("=", $line);
|
||||||
|
$config[$arrLine[0]] = $arrLine[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a wireless centre frequency to a friendly channel label.
|
||||||
|
*
|
||||||
|
* Accepts either MHz integers (e.g. "2412" → "2.4GHz Ch1", "5180" →
|
||||||
|
* "5.0GHz Ch36") or some legacy short forms emitted by older `iw`
|
||||||
|
* builds (e.g. "504" / "508" for 5GHz channels in the 5MHz grid).
|
||||||
|
*
|
||||||
|
* @param string $freq Centre frequency as reported by iwconfig/iw.
|
||||||
|
* @return string Friendly channel label, or "Invalid Channel" if unmapped.
|
||||||
|
*/
|
||||||
|
function ConvertToChannel($freq)
|
||||||
|
{
|
||||||
|
$wifiFreqToChan = array (
|
||||||
|
"2412" => "2.4GHz Ch1",
|
||||||
|
"2417" => "2.4GHz Ch2",
|
||||||
|
"2422" => "2.4GHz Ch3",
|
||||||
|
"2427" => "2.4GHz Ch4",
|
||||||
|
"2432" => "2.4GHz Ch5",
|
||||||
|
"2437" => "2.4GHz Ch6",
|
||||||
|
"2442" => "2.4GHz Ch7",
|
||||||
|
"2447" => "2.4GHz Ch8",
|
||||||
|
"2452" => "2.4GHz Ch9",
|
||||||
|
"2457" => "2.4GHz Ch10",
|
||||||
|
"2462" => "2.4GHz Ch11",
|
||||||
|
"2467" => "2.4GHz Ch12",
|
||||||
|
"2472" => "2.4GHz Ch13",
|
||||||
|
"2484" => "2.4GHz Ch14",
|
||||||
|
"504" => "5.0GHz Ch8",
|
||||||
|
"506" => "5.0GHz Ch12",
|
||||||
|
"508" => "5.0GHz Ch16",
|
||||||
|
"517" => "5.0GHz Ch34",
|
||||||
|
"518" => "5.0GHz Ch36",
|
||||||
|
"519" => "5.0GHz Ch38",
|
||||||
|
"520" => "5.0GHz Ch40",
|
||||||
|
"521" => "5.0GHz Ch42",
|
||||||
|
"522" => "5.0GHz Ch44",
|
||||||
|
"523" => "5.0GHz Ch46",
|
||||||
|
"524" => "5.0GHz Ch48",
|
||||||
|
"526" => "5.0GHz Ch52",
|
||||||
|
"528" => "5.0GHz Ch56",
|
||||||
|
"530" => "5.0GHz Ch60",
|
||||||
|
"532" => "5.0GHz Ch64",
|
||||||
|
"550" => "5.0GHz Ch100",
|
||||||
|
"552" => "5.0GHz Ch104",
|
||||||
|
"554" => "5.0GHz Ch108",
|
||||||
|
"556" => "5.0GHz Ch112",
|
||||||
|
"558" => "5.0GHz Ch116",
|
||||||
|
"560" => "5.0GHz Ch120",
|
||||||
|
"562" => "5.0GHz Ch124",
|
||||||
|
"564" => "5.0GHz Ch128",
|
||||||
|
"566" => "5.0GHz Ch132",
|
||||||
|
"568" => "5.0GHz Ch136",
|
||||||
|
"570" => "5.0GHz Ch140",
|
||||||
|
"492" => "5.0GHz Ch184",
|
||||||
|
"494" => "5.0GHz Ch188",
|
||||||
|
"496" => "5.0GHz Ch192",
|
||||||
|
"498" => "5.0GHz Ch196",
|
||||||
|
"5035" => "5.0GHz Ch7",
|
||||||
|
"5040" => "5.0GHz Ch8",
|
||||||
|
"5045" => "5.0GHz Ch9",
|
||||||
|
"5055" => "5.0GHz Ch11",
|
||||||
|
"5060" => "5.0GHz Ch12",
|
||||||
|
"5080" => "5.0GHz Ch16",
|
||||||
|
"5170" => "5.0GHz Ch34",
|
||||||
|
"5180" => "5.0GHz Ch36",
|
||||||
|
"5190" => "5.0GHz Ch38",
|
||||||
|
"5200" => "5.0GHz Ch40",
|
||||||
|
"5210" => "5.0GHz Ch42",
|
||||||
|
"5220" => "5.0GHz Ch44",
|
||||||
|
"5230" => "5.0GHz Ch46",
|
||||||
|
"5240" => "5.0GHz Ch48",
|
||||||
|
"5260" => "5.0GHz Ch52",
|
||||||
|
"5280" => "5.0GHz Ch56",
|
||||||
|
"5300" => "5.0GHz Ch60",
|
||||||
|
"5320" => "5.0GHz Ch64",
|
||||||
|
"5500" => "5.0GHz Ch100",
|
||||||
|
"5520" => "5.0GHz Ch104",
|
||||||
|
"5540" => "5.0GHz Ch108",
|
||||||
|
"5560" => "5.0GHz Ch112",
|
||||||
|
"5580" => "5.0GHz Ch116",
|
||||||
|
"5600" => "5.0GHz Ch120",
|
||||||
|
"5620" => "5.0GHz Ch124",
|
||||||
|
"5640" => "5.0GHz Ch128",
|
||||||
|
"5660" => "5.0GHz Ch132",
|
||||||
|
"5680" => "5.0GHz Ch136",
|
||||||
|
"5700" => "5.0GHz Ch140",
|
||||||
|
"5745" => "5.0GHz Ch149",
|
||||||
|
"5765" => "5.0GHz Ch153",
|
||||||
|
"5785" => "5.0GHz Ch157",
|
||||||
|
"5805" => "5.0GHz Ch161",
|
||||||
|
"5825" => "5.0GHz Ch165",
|
||||||
|
"4915" => "5.0GHz Ch183",
|
||||||
|
"4920" => "5.0GHz Ch184",
|
||||||
|
"4925" => "5.0GHz Ch185",
|
||||||
|
"4935" => "5.0GHz Ch187",
|
||||||
|
"4940" => "5.0GHz Ch188",
|
||||||
|
"4945" => "5.0GHz Ch189",
|
||||||
|
"4960" => "5.0GHz Ch192",
|
||||||
|
"4980" => "5.0GHz Ch196"
|
||||||
|
);
|
||||||
|
if (array_key_exists($freq, $wifiFreqToChan)) { return $wifiFreqToChan[$freq]; }
|
||||||
|
else { return "Invalid Channel"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a wpa_supplicant security flag string to a human-friendly label.
|
||||||
|
*
|
||||||
|
* Input is the bracketed token list reported by `wpa_cli scan_results`
|
||||||
|
* (e.g. "[WPA2-PSK-CCMP][ESS]" → "WPA2-PSK (AES)"). Unknown flag strings
|
||||||
|
* fall through to the default branch and are returned verbatim so the
|
||||||
|
* UI never silently swallows a network it doesn't recognise.
|
||||||
|
*
|
||||||
|
* @param string $security Bracketed flag list from wpa_cli output.
|
||||||
|
* @return string Friendly security descriptor, or the raw flag string.
|
||||||
|
*/
|
||||||
|
function ConvertToSecurity($security)
|
||||||
|
{
|
||||||
|
switch ($security) {
|
||||||
|
case "[WPA2-PSK-CCMP][ESS]":
|
||||||
|
return "WPA2-PSK (AES)";
|
||||||
|
break;
|
||||||
|
case "[WPA2-PSK-CCMP-preauth][ESS]":
|
||||||
|
return "WPA2-PSK (AES) with Preauth";
|
||||||
|
break;
|
||||||
|
case "[WPA2-PSK-TKIP][ESS]":
|
||||||
|
return "WPA2-PSK (TKIP)";
|
||||||
|
break;
|
||||||
|
case "[WPA2-PSK-CCMP][WPS][ESS]":
|
||||||
|
return "WPA2-PSK (TKIP) with WPS";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-TKIP+CCMP][WPS][ESS]":
|
||||||
|
return "WPA-PSK (TKIP/AES) with WPS";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS]":
|
||||||
|
return "WPA/WPA2-PSK (TKIP)";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-TKIP+CCMP][WPA2-PSK-TKIP+CCMP][ESS]":
|
||||||
|
return "WPA/WPA2-PSK (TKIP/AES)";
|
||||||
|
break;
|
||||||
|
case "[WPA-EAP-CCMP+TKIP][WPA2-EAP-CCMP+TKIP-preauth][ESS]":
|
||||||
|
return "WPA/WPA2-PSK (TKIP/AES) with Preauth";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][WPS][ESS]":
|
||||||
|
return "WPA/WPA2-PSK (TKIP/AES) with WPS";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-CCMP][WPA2-PSK-CCMP][WPS][ESS]":
|
||||||
|
return "WPA/WPA2-PSK (AES) with WPS";
|
||||||
|
break;
|
||||||
|
case "[WPA-PSK-TKIP][ESS]":
|
||||||
|
return "WPA-PSK (TKIP)";
|
||||||
|
break;
|
||||||
|
case "[WEP][ESS]":
|
||||||
|
return "WEP";
|
||||||
|
break;
|
||||||
|
case "[ESS]":
|
||||||
|
return "None";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return $security;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
body {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color:red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
color:green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
border:1px solid;
|
||||||
|
text-align:left;
|
||||||
|
padding-top:5px;
|
||||||
|
padding-left:5px;
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableft {
|
||||||
|
display:inline-block;
|
||||||
|
text-align:right;
|
||||||
|
width:120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width:699px;
|
||||||
|
height:499px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.button {
|
||||||
|
width:80%;
|
||||||
|
border:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],input[type=password] {
|
||||||
|
border:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button],input[type=submit] {
|
||||||
|
border:1px solid;
|
||||||
|
border-radius:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoheader {
|
||||||
|
width:100%;
|
||||||
|
text-align:center;
|
||||||
|
margin-top:10px;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
font-weight: bold;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intinfo {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
color: #000000;
|
||||||
|
width: 398px;
|
||||||
|
text-align: left;
|
||||||
|
border-right: 1px solid;
|
||||||
|
border-left: 1px solid;
|
||||||
|
float: left;
|
||||||
|
border-bottom:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifiinfo {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0 0 0 400px;
|
||||||
|
border-right: 1px solid;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intheader {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align:center;
|
||||||
|
width:100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intfooter {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
color: #ffffff;
|
||||||
|
width:100%;
|
||||||
|
border-top:1px solid;
|
||||||
|
float:left;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tail {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
color: #dd4b39;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WiFi-config page stylesheet.
|
||||||
|
*
|
||||||
|
* Emits CSS to the browser with a `Content-type: text/css` header. Used
|
||||||
|
* exclusively by admin/wifi.php to style the WiFi configuration UI
|
||||||
|
* (network list cards, scan results, info panels). Theme colours are
|
||||||
|
* loaded from /etc/pistar-css.ini (operator-overridable via
|
||||||
|
* admin/expert/edit_dashboard.php) when that file exists, otherwise
|
||||||
|
* fall back to the built-in palette below.
|
||||||
|
*
|
||||||
|
* The PHP header here is a near-duplicate of css/pistar-css.php's
|
||||||
|
* theme-load block — kept in lockstep with that file. The CSS body is
|
||||||
|
* WiFi-specific and intentionally diverges.
|
||||||
|
*
|
||||||
|
* Do NOT edit this file by hand. The same long-standing typos in
|
||||||
|
* variable names ($bannerDropShaddows, $tableHeadDropShaddow) are
|
||||||
|
* preserved here because the CSS body interpolates them by name —
|
||||||
|
* see the matching note in css/pistar-css.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Output CSS, not the default text/html.
|
||||||
|
header('Content-type: text/css');
|
||||||
|
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
// Load the operator's theme overrides.
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile, 'r')) {
|
||||||
|
$piStarCss = parse_ini_file($piStarCssFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map parsed INI values onto the variables the CSS body interpolates.
|
||||||
|
$backgroundPage = $piStarCss['Background']['Page']; // page background (usually off-white)
|
||||||
|
$backgroundContent = $piStarCss['Background']['Content']; // white background in the content section
|
||||||
|
$backgroundBanners = $piStarCss['Background']['Banners']; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = $piStarCss['Text']['Banners']; // usually white
|
||||||
|
$bannerDropShaddows = $piStarCss['Text']['BannersDrop']; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = $piStarCss['Tables']['HeadDrop']; // table-header drop-shadow colour
|
||||||
|
$textContent = $piStarCss['Content']['Text']; // section title colour
|
||||||
|
$tableRowEvenBg = $piStarCss['Tables']['BgEven']; // table row background (even)
|
||||||
|
$tableRowOddBg = $piStarCss['Tables']['BgOdd']; // table row background (odd)
|
||||||
|
} else {
|
||||||
|
// Fallback palette used when /etc/pistar-css.ini is absent.
|
||||||
|
$backgroundPage = 'edf0f5'; // page background (usually off-white)
|
||||||
|
$backgroundContent = 'ffffff'; // white background in the content section
|
||||||
|
$backgroundBanners = 'dd4b39'; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = 'ffffff'; // usually white
|
||||||
|
$bannerDropShaddows = '303030'; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = '8b0000'; // table-header drop-shadow colour
|
||||||
|
$textContent = '000000'; // section title colour
|
||||||
|
$tableRowEvenBg = 'f7f7f7'; // table row background (even)
|
||||||
|
$tableRowOddBg = 'd0d0d0'; // table row background (odd)
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
body {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color:red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
color:green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
border:1px solid;
|
||||||
|
text-align:left;
|
||||||
|
padding-top:5px;
|
||||||
|
padding-left:5px;
|
||||||
|
background-color: #<?php echo $tableRowOddBg; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableft {
|
||||||
|
display:inline-block;
|
||||||
|
text-align:right;
|
||||||
|
width:120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width:699px;
|
||||||
|
height:499px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.button {
|
||||||
|
width:80%;
|
||||||
|
border:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],input[type=password] {
|
||||||
|
border:1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button],input[type=submit] {
|
||||||
|
border:1px solid;
|
||||||
|
border-radius:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoheader {
|
||||||
|
width:100%;
|
||||||
|
text-align:center;
|
||||||
|
margin-top:10px;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
font-weight: bold;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intinfo {
|
||||||
|
background-color: #<?php echo $tableRowOddBg; ?>;
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
width: 398px;
|
||||||
|
text-align: left;
|
||||||
|
border-right: 1px solid;
|
||||||
|
border-left: 1px solid;
|
||||||
|
float: left;
|
||||||
|
border-bottom:1px solid;
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifiinfo {
|
||||||
|
background-color: #<?php echo $tableRowOddBg; ?>;
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
margin: 0 0 0 400px;
|
||||||
|
border-right: 1px solid;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom:1px solid;
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intheader {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
color: #<?php echo $textBanners; ?>;
|
||||||
|
text-align:center;
|
||||||
|
width:100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intfooter {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
color: #<?php echo $textBanners; ?>;
|
||||||
|
width:100%;
|
||||||
|
border-top:1px solid;
|
||||||
|
float:left;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tail {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JSON API: recent transmissions ("last heard" feed).
|
||||||
|
*
|
||||||
|
* The only JSON endpoint exposed by the dashboard. Returns the in-memory
|
||||||
|
* `$lastHeard` array — populated as a side effect of including
|
||||||
|
* `mmdvmhost/functions.php` — as a JSON array, newest call first.
|
||||||
|
*
|
||||||
|
* Query string:
|
||||||
|
* ?num_transmissions=N Cap the number of entries returned. When
|
||||||
|
* omitted, every entry currently in memory is
|
||||||
|
* returned (typically the last 30 calls).
|
||||||
|
*
|
||||||
|
* Response shape — array of objects with these string fields:
|
||||||
|
* time_utc, mode, callsign, callsign_suffix, target, src,
|
||||||
|
* duration, loss, bit_error_rate, rssi
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* GET /api/last_heard.php?num_transmissions=5
|
||||||
|
*
|
||||||
|
* NOTE: this endpoint does not currently call any of the
|
||||||
|
* config/security_headers.php helpers — flagged for the security pass.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'] . '/config/config.php'; // Dashboard runtime constants
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'] . '/mmdvmhost/tools.php'; // Helpers (format_time, isProcessRunning, ...)
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'] . '/mmdvmhost/functions.php'; // Populates $lastHeard
|
||||||
|
|
||||||
|
header('Content-type: application/json');
|
||||||
|
|
||||||
|
$json_response = array();
|
||||||
|
|
||||||
|
$trans_history_count = count($lastHeard);
|
||||||
|
|
||||||
|
// Cap to caller's requested count, but never exceed what's in memory.
|
||||||
|
// `intval()` defends against non-numeric query strings (returns 0).
|
||||||
|
$num_transmissions = isset($_GET['num_transmissions'])
|
||||||
|
? intval($_GET['num_transmissions'])
|
||||||
|
: $trans_history_count;
|
||||||
|
$transmissions = array_slice($lastHeard, 0, min($num_transmissions, $trans_history_count));
|
||||||
|
|
||||||
|
// $lastHeard is a positional array (one row per call) where each row is
|
||||||
|
// itself a positional array. The index → field name mapping below is the
|
||||||
|
// stable contract for this endpoint; do not reorder without bumping a
|
||||||
|
// version, callers depend on these names.
|
||||||
|
foreach ($transmissions as $transmission) {
|
||||||
|
$transmission_json = array();
|
||||||
|
$transmission_json['time_utc'] = trim($transmission[0]);
|
||||||
|
$transmission_json['mode'] = trim($transmission[1]);
|
||||||
|
$transmission_json['callsign'] = trim($transmission[2]);
|
||||||
|
$transmission_json['callsign_suffix'] = trim($transmission[3]);
|
||||||
|
$transmission_json['target'] = trim($transmission[4]);
|
||||||
|
$transmission_json['src'] = trim($transmission[5]);
|
||||||
|
$transmission_json['duration'] = trim($transmission[6]);
|
||||||
|
$transmission_json['loss'] = trim($transmission[7]);
|
||||||
|
$transmission_json['bit_error_rate'] = trim($transmission[8]);
|
||||||
|
$transmission_json['rssi'] = trim($transmission[9]);
|
||||||
|
|
||||||
|
$json_response[] = $transmission_json;
|
||||||
|
}
|
||||||
|
echo json_encode($json_response);
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Single source of truth for site-wide warning banners and the
|
||||||
|
* default-password forced-redirect.
|
||||||
|
*
|
||||||
|
* Two public entry points:
|
||||||
|
*
|
||||||
|
* pistar_warnings_enforce_redirect()
|
||||||
|
* Call ONCE near the top of every authenticated page, BEFORE
|
||||||
|
* any output. Issues a 302 + exit if the operator is on the
|
||||||
|
* default password and connecting from a remote subnet. Wired
|
||||||
|
* to /admin/* URLs only — the public root `/` is unauthenticated
|
||||||
|
* and we deliberately do NOT advertise default-password state
|
||||||
|
* to anonymous visitors.
|
||||||
|
*
|
||||||
|
* pistar_warnings_render()
|
||||||
|
* Call from the body to emit any/all of the seven warning
|
||||||
|
* banners that currently apply. Renders nothing on the public
|
||||||
|
* root (warnings target the operator, not the public).
|
||||||
|
*
|
||||||
|
* Banners managed here (consolidated from index.php and configure.php):
|
||||||
|
* 1. Pi-Star outdated (<4.1 on RPi) yellow
|
||||||
|
* 2. Upgrade available 4.1.0..<4.1.13 yellow
|
||||||
|
* 3. Upgrade available 4.2.0..<4.2.6 yellow
|
||||||
|
* 4. Upgrade available 4.3.0..<4.3.7 yellow
|
||||||
|
* 5. DMR public mode without ACL (loop risk) yellow
|
||||||
|
* 6. BM API v1 key still in use red
|
||||||
|
* 7. Default basic-auth password ('raspberry') red
|
||||||
|
*
|
||||||
|
* Layer 2 (forced redirect) precedence — most → least specific:
|
||||||
|
* - Setup-not-done flow in index.php takes priority. Redirect is
|
||||||
|
* suppressed when neither /etc/dstar-radio.{mmdvmhost,dstarrepeater}
|
||||||
|
* marker exists, so the existing "No Mode Defined" 10-second
|
||||||
|
* redirect to configure.php remains the dominant path.
|
||||||
|
* - Default password + remote client → 302 to /admin/change_password_required.php.
|
||||||
|
* - Default password + local client → red banner only (no redirect).
|
||||||
|
* - Custom password → no warning, no redirect.
|
||||||
|
*
|
||||||
|
* Helpers (private; underscore prefix):
|
||||||
|
* _pistar_banner_emit() — render one banner row
|
||||||
|
* _pistar_get_basic_auth() — extract user/password from request
|
||||||
|
* _pistar_default_password_in_use() — true iff pi-star user using 'raspberry'
|
||||||
|
* _pistar_local_subnets() — Pi's interface CIDRs (IPv4 + IPv6)
|
||||||
|
* _pistar_ip_in_subnet() — bitmask compare, family-aware
|
||||||
|
* _pistar_remote_is_local() — REMOTE_ADDR ∈ any local subnet
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single banner row.
|
||||||
|
*
|
||||||
|
* Matches the inline-styled <div><table> shape used throughout the
|
||||||
|
* dashboard (no CSS class — same convention as the existing banners
|
||||||
|
* in index.php / configure.php this file replaces).
|
||||||
|
*
|
||||||
|
* @param string $html Banner body. INTERPOLATED RAW — callers must
|
||||||
|
* ensure any dynamic content is already escaped
|
||||||
|
* (none of the current banners include dynamic
|
||||||
|
* content, so this is a non-issue today; flagged
|
||||||
|
* for any future caller).
|
||||||
|
* @param string $level 'warn' (yellow) or 'danger' (red).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function _pistar_banner_emit($html, $level = 'warn')
|
||||||
|
{
|
||||||
|
$style = ($level === 'danger')
|
||||||
|
? 'background-color: #ff9090; color: #f01010;'
|
||||||
|
: 'background-color: #ffff90; color: #906000;';
|
||||||
|
echo '<div>' . "\n";
|
||||||
|
echo ' <table align="center" width="760px" style="margin: 0px 0px 10px 0px; width: 100%;">' . "\n";
|
||||||
|
echo ' <tr>' . "\n";
|
||||||
|
echo ' <td align="center" valign="top" style="' . $style . '">' . $html . '</td>' . "\n";
|
||||||
|
echo ' </tr>' . "\n";
|
||||||
|
echo ' </table>' . "\n";
|
||||||
|
echo '</div>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the HTTP Basic auth user + password from the current request.
|
||||||
|
*
|
||||||
|
* On Pi-Star's stock nginx + PHP-FPM config, $_SERVER['PHP_AUTH_PW']
|
||||||
|
* is populated automatically (PHP synthesises it from the forwarded
|
||||||
|
* Authorization header). The HTTP_AUTHORIZATION fallback handles
|
||||||
|
* unusual setups where only the raw header survives.
|
||||||
|
*
|
||||||
|
* Note: the REMOTE_USER fallback only sets $user — that var contains
|
||||||
|
* the authenticated username but never the password. Callers that need
|
||||||
|
* the password (default-password check, etc.) treat empty $pw as
|
||||||
|
* "unable to verify" and fail closed.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: string} [$user, $password]; either or
|
||||||
|
* both may be '' when basic auth
|
||||||
|
* hasn't happened (e.g. on the
|
||||||
|
* unauthenticated public root).
|
||||||
|
*/
|
||||||
|
function _pistar_get_basic_auth()
|
||||||
|
{
|
||||||
|
$user = isset($_SERVER['PHP_AUTH_USER']) ? (string)$_SERVER['PHP_AUTH_USER'] : '';
|
||||||
|
$pw = isset($_SERVER['PHP_AUTH_PW']) ? (string)$_SERVER['PHP_AUTH_PW'] : '';
|
||||||
|
if ($user !== '' && $pw !== '') {
|
||||||
|
return array($user, $pw);
|
||||||
|
}
|
||||||
|
if (!empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||||
|
&& stripos($_SERVER['HTTP_AUTHORIZATION'], 'Basic ') === 0) {
|
||||||
|
$decoded = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6), true);
|
||||||
|
if ($decoded !== false && strpos($decoded, ':') !== false) {
|
||||||
|
list($user, $pw) = explode(':', $decoded, 2);
|
||||||
|
return array($user, $pw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($user === '' && !empty($_SERVER['REMOTE_USER'])) {
|
||||||
|
$user = (string)$_SERVER['REMOTE_USER'];
|
||||||
|
}
|
||||||
|
return array($user, $pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True iff the operator is the canonical 'pi-star' account AND
|
||||||
|
* still using the factory default password 'raspberry'.
|
||||||
|
*
|
||||||
|
* Account scope is deliberately narrow: the requirement is to
|
||||||
|
* protect against drive-by auth using the well-known stock
|
||||||
|
* credentials. Operators who created their own usernames are
|
||||||
|
* out of scope (they made an active choice).
|
||||||
|
*
|
||||||
|
* Constant-time compare via hash_equals — defence in depth; the
|
||||||
|
* default password is publicly documented so timing isn't really
|
||||||
|
* an oracle, but it's the right idiom for password compares.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function _pistar_default_password_in_use()
|
||||||
|
{
|
||||||
|
list($user, $pw) = _pistar_get_basic_auth();
|
||||||
|
if ($user !== 'pi-star' || $pw === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hash_equals('raspberry', $pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Pi's local interface subnets (IPv4 + IPv6).
|
||||||
|
*
|
||||||
|
* Read once per request from `ip -j addr show` (iproute2 JSON output;
|
||||||
|
* always present on Pi-Star). Cached in a static for repeated calls.
|
||||||
|
*
|
||||||
|
* @return array<int, array{ip:string, prefix:int}>
|
||||||
|
*/
|
||||||
|
function _pistar_local_subnets()
|
||||||
|
{
|
||||||
|
static $cached = null;
|
||||||
|
if ($cached !== null) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
$cached = array();
|
||||||
|
|
||||||
|
$out = array();
|
||||||
|
$rc = 1;
|
||||||
|
exec('ip -j addr show 2>/dev/null', $out, $rc);
|
||||||
|
if ($rc !== 0 || empty($out)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
$ifaces = json_decode(implode('', $out), true);
|
||||||
|
if (!is_array($ifaces)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
foreach ($ifaces as $iface) {
|
||||||
|
if (empty($iface['addr_info']) || !is_array($iface['addr_info'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($iface['addr_info'] as $a) {
|
||||||
|
if (empty($a['local']) || !isset($a['prefixlen'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cached[] = array(
|
||||||
|
'ip' => (string)$a['local'],
|
||||||
|
'prefix' => (int)$a['prefixlen'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Family-aware bitmask subnet membership test.
|
||||||
|
*
|
||||||
|
* Handles IPv4, IPv6, and the IPv4-mapped IPv6 form (`::ffff:a.b.c.d`)
|
||||||
|
* that nginx may surface in REMOTE_ADDR when the listener is dual-stack.
|
||||||
|
*
|
||||||
|
* @param string $remote The address to test.
|
||||||
|
* @param string $netIp The subnet's network/anchor address.
|
||||||
|
* @param int $prefix Prefix length in bits (0..32 for IPv4, 0..128 for IPv6).
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function _pistar_ip_in_subnet($remote, $netIp, $prefix)
|
||||||
|
{
|
||||||
|
$remoteBin = @inet_pton($remote);
|
||||||
|
$netBin = @inet_pton($netIp);
|
||||||
|
if ($remoteBin === false || $netBin === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Handle v4-mapped v6 (::ffff:1.2.3.4) when comparing against a
|
||||||
|
// bare v4 subnet — strip the 12-byte v4-mapped prefix. Mapping is
|
||||||
|
// one-directional by design: nginx surfaces REMOTE_ADDR in mapped
|
||||||
|
// form on dual-stack listeners while `ip -j addr show` reports the
|
||||||
|
// interface as bare v4, so the asymmetric strip is what the real
|
||||||
|
// topology produces. The reverse case (bare v4 client, mapped
|
||||||
|
// subnet) cannot occur in practice and falls through to "not a
|
||||||
|
// match" below.
|
||||||
|
if (strlen($remoteBin) === 16 && strlen($netBin) === 4
|
||||||
|
&& substr($remoteBin, 0, 12) === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||||
|
$remoteBin = substr($remoteBin, 12);
|
||||||
|
}
|
||||||
|
if (strlen($remoteBin) !== strlen($netBin)) {
|
||||||
|
// Different families and no compatible mapping — not in subnet.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$bits = strlen($remoteBin) * 8;
|
||||||
|
if ($prefix < 0 || $prefix > $bits) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$bytesFull = intdiv($prefix, 8);
|
||||||
|
$bitsExtra = $prefix % 8;
|
||||||
|
if ($bytesFull > 0
|
||||||
|
&& substr($remoteBin, 0, $bytesFull) !== substr($netBin, 0, $bytesFull)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($bitsExtra > 0) {
|
||||||
|
$mask = chr((0xFF << (8 - $bitsExtra)) & 0xFF);
|
||||||
|
if ((substr($remoteBin, $bytesFull, 1) & $mask)
|
||||||
|
!== (substr($netBin, $bytesFull, 1) & $mask)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is REMOTE_ADDR in the same L3 subnet as ANY of the Pi's interfaces?
|
||||||
|
*
|
||||||
|
* Loopback (127.0.0.0/8 on v4, ::1/128 on v6) is treated as local —
|
||||||
|
* those addresses appear when an admin page is hit via the device
|
||||||
|
* itself or via a CLI test harness on the Pi.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function _pistar_remote_is_local()
|
||||||
|
{
|
||||||
|
$remote = isset($_SERVER['REMOTE_ADDR']) ? (string)$_SERVER['REMOTE_ADDR'] : '';
|
||||||
|
if ($remote === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (_pistar_local_subnets() as $sub) {
|
||||||
|
if (_pistar_ip_in_subnet($remote, $sub['ip'], $sub['prefix'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layer 2: redirect to the forced-change page when default password +
|
||||||
|
* remote client. Must be called BEFORE any output so header() works.
|
||||||
|
*
|
||||||
|
* Skips:
|
||||||
|
* - Public (non-/admin/) URLs — anonymous visitors don't get told
|
||||||
|
* the device is on default credentials.
|
||||||
|
* - The /admin/change_password_required.php page itself — would
|
||||||
|
* loop.
|
||||||
|
* - Pages reached during the setup-not-done flow (no mode marker
|
||||||
|
* file yet) — let the existing "No Mode Defined" handler in
|
||||||
|
* index.php drive the operator through configure.php first.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function pistar_warnings_enforce_redirect()
|
||||||
|
{
|
||||||
|
$self = isset($_SERVER['PHP_SELF']) ? (string)$_SERVER['PHP_SELF'] : '';
|
||||||
|
if (strpos($self, '/admin/') !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($self === '/admin/change_password_required.php') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Long-running operational tools — let the operator finish what
|
||||||
|
// they're doing. The Layer-1 banner still appears at the top of
|
||||||
|
// these pages so the warning isn't suppressed; only the redirect
|
||||||
|
// is. update.php and expert/upgrade.php both poll a `?ajax`
|
||||||
|
// log-tail every 1 s; redirecting on those polls causes jQuery
|
||||||
|
// to follow the 302 and append the full change-password HTML
|
||||||
|
// into the page's #tail div, "flooding" the operator with copies
|
||||||
|
// of the change-password form.
|
||||||
|
if ($self === '/admin/update.php'
|
||||||
|
|| $self === '/admin/expert/upgrade.php') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Defence in depth: any GET that includes ?ajax skips the redirect.
|
||||||
|
// Catches AJAX log-tail / status-poll endpoints elsewhere in the
|
||||||
|
// dashboard (live_modem_log.php, expert/jitter_test.php, etc.) so
|
||||||
|
// a future poll-style page can't accidentally regress the same way.
|
||||||
|
// The initial GET that loaded the parent page already carried the
|
||||||
|
// banner — defaulting AJAX to "no redirect" is safe.
|
||||||
|
if (isset($_GET['ajax'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!file_exists('/etc/dstar-radio.mmdvmhost')
|
||||||
|
&& !file_exists('/etc/dstar-radio.dstarrepeater')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_pistar_default_password_in_use()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_pistar_remote_is_local()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
header('Location: /admin/change_password_required.php', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render every applicable warning banner. Admin URLs only.
|
||||||
|
*
|
||||||
|
* Adding a new banner here? The body string is interpolated into the
|
||||||
|
* <td> raw — pre-escape any dynamic content with htmlspecialchars()
|
||||||
|
* before passing it to _pistar_banner_emit(). Today's seven banners
|
||||||
|
* are all string literals so the issue does not arise.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function pistar_warnings_render()
|
||||||
|
{
|
||||||
|
$self = isset($_SERVER['PHP_SELF']) ? (string)$_SERVER['PHP_SELF'] : '';
|
||||||
|
if (strpos($self, '/admin/') !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default-password banner FIRST — highest severity, most actionable.
|
||||||
|
if (_pistar_default_password_in_use()) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'<b>Security Alert:</b> Your dashboard is using the default password. '
|
||||||
|
. 'Please <a href="/admin/configure.php" style="color: #f01010;"><b>change it now</b></a> '
|
||||||
|
. 'from the Configuration page.',
|
||||||
|
'danger'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pi-Star release-derived banners. parse_ini_file() once per request.
|
||||||
|
$release = @parse_ini_file('/etc/pistar-release', true);
|
||||||
|
$version = is_array($release) && isset($release['Pi-Star']['Version'])
|
||||||
|
? (string)$release['Pi-Star']['Version']
|
||||||
|
: '';
|
||||||
|
$hardware = is_array($release) && isset($release['Pi-Star']['Hardware'])
|
||||||
|
? (string)$release['Pi-Star']['Hardware']
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if ($version !== '' && $hardware === 'RPi'
|
||||||
|
&& version_compare($version, '4.1', '<')) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: You are running an outdated version of Pi-Star, please upgrade.<br />'
|
||||||
|
. 'New versions are available from here: '
|
||||||
|
. '<a href="http://www.pistar.uk/downloads/" alt="Pi-Star Downloads">http://www.pistar.uk/downloads/</a>.',
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($version !== ''
|
||||||
|
&& version_compare($version, '4.1.0', '>=')
|
||||||
|
&& version_compare($version, '4.1.13', '<')) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: An upgrade to Pi-Star has been released, click here to upgrade now: '
|
||||||
|
. '<a href="/admin/expert/upgrade.php" alt="Upgrade Pi-Star">Upgrade Pi-Star</a>.',
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($version !== ''
|
||||||
|
&& version_compare($version, '4.2.0', '>=')
|
||||||
|
&& version_compare($version, '4.2.6', '<')) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: An upgrade to Pi-Star has been released, click here to upgrade now: '
|
||||||
|
. '<a href="/admin/expert/upgrade.php" alt="Upgrade Pi-Star">Upgrade Pi-Star</a>.',
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($version !== ''
|
||||||
|
&& version_compare($version, '4.3.0', '>=')
|
||||||
|
&& version_compare($version, '4.3.7', '<')) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: An upgrade to Pi-Star has been released, click here to upgrade now: '
|
||||||
|
. '<a href="/admin/expert/upgrade.php" alt="Upgrade Pi-Star">Upgrade Pi-Star</a>.',
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMR public mode without ACL (loop risk). Was configure.php-only;
|
||||||
|
// now appears across the admin surface so the operator sees it
|
||||||
|
// regardless of which page they land on first.
|
||||||
|
if (file_exists('/etc/dstar-radio.mmdvmhost')) {
|
||||||
|
$mmdvm = @parse_ini_file('/etc/mmdvmhost', true);
|
||||||
|
if (is_array($mmdvm)
|
||||||
|
&& isset($mmdvm['DMR']['Enable']) && $mmdvm['DMR']['Enable'] == 1
|
||||||
|
&& isset($mmdvm['DMR']['SelfOnly']) && $mmdvm['DMR']['SelfOnly'] == 0
|
||||||
|
&& isset($mmdvm['General']['Id'])
|
||||||
|
&& strlen((string)$mmdvm['General']['Id']) >= 7
|
||||||
|
&& !isset($mmdvm['DMR']['WhiteList'])) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: You are running a hotspot in public mode without an access list for DMR, '
|
||||||
|
. 'this setup *could* participate in network loops!',
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BM API v1 key (length-based heuristic, kept identical to the
|
||||||
|
// configure.php trigger this consolidates).
|
||||||
|
$bmKey = '/etc/bmapi.key';
|
||||||
|
if (file_exists($bmKey)) {
|
||||||
|
$bm = @parse_ini_file($bmKey, true);
|
||||||
|
if (is_array($bm) && !empty($bm['key']['apikey'])
|
||||||
|
&& strlen((string)$bm['key']['apikey']) <= 200) {
|
||||||
|
_pistar_banner_emit(
|
||||||
|
'Alert: You have a BM API v1 Key, click here for the announcement: '
|
||||||
|
. '<a href="https://news.brandmeister.network/introducing-user-api-keys/" alt="BM API Keys">BM API Keys - Announcement</a>.',
|
||||||
|
'danger'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile-vs-desktop CSS picker.
|
||||||
|
*
|
||||||
|
* Inspects the User-Agent string and emits two `<link rel="stylesheet">`
|
||||||
|
* tags wired up with media queries: `pistar-css.php` for desktop widths,
|
||||||
|
* `pistar-css-mini.php` for narrow / mobile widths.
|
||||||
|
*
|
||||||
|
* The big regex in this file is the standard "detectmobilebrowsers"
|
||||||
|
* UA list (https://detectmobilebrowsers.com/) and is intentionally not
|
||||||
|
* formatted — it's a single literal that must stay byte-identical to
|
||||||
|
* remain a valid match for the documented agent list.
|
||||||
|
*
|
||||||
|
* Included from the page <head> by index.php / admin/index.php and
|
||||||
|
* the configure / power / update pages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (empty($_SERVER['HTTP_USER_AGENT'])) {
|
||||||
|
// No UA at all (curl, some CLIs, oddball clients): default to the
|
||||||
|
// desktop-vs-mobile width-based pair so behaviour matches a normal
|
||||||
|
// browser hitting the page.
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (min-width: 830px)\" href=\"css/pistar-css.php?version=0.95\" />\n";
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-width: 829px)\" href=\"css/pistar-css-mini.php?version=0.95\" />\n";
|
||||||
|
} else {
|
||||||
|
$useragent = $_SERVER['HTTP_USER_AGENT'];
|
||||||
|
if (preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4))) {
|
||||||
|
// Mobile UA detected — bias toward the narrow-screen stylesheet.
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (min-device-width: 380px)\" href=\"css/pistar-css.php?version=0.95\" />\n";
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-device-width: 379px)\" href=\"css/pistar-css-mini.php?version=0.95\" />\n";
|
||||||
|
} else {
|
||||||
|
// Desktop UA — width-based pair (same as the no-UA branch).
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (min-width: 830px)\" href=\"css/pistar-css.php?version=0.95\" />\n";
|
||||||
|
print " <link rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-width: 829px)\" href=\"css/pistar-css-mini.php?version=0.95\" />\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pi-Star Dashboard runtime constants.
|
||||||
|
*
|
||||||
|
* Auto-generated — be careful editing by hand; configure.php may
|
||||||
|
* regenerate parts of this file when the operator changes settings.
|
||||||
|
*
|
||||||
|
* Defines paths to MMDVMHost / YSF / P25 gateway logs and config files.
|
||||||
|
* Consumed primarily by mmdvmhost/functions.php for the data-extraction
|
||||||
|
* layer (last-heard parsing, link state lookup, mode detection).
|
||||||
|
*
|
||||||
|
* The trailing `REBOOT*` / `HALTSYS` / `TEMPERATUREHIGHLEVEL` defines
|
||||||
|
* are placeholders kept blank by default; some legacy paths inspect
|
||||||
|
* them via `defined()`/`!empty()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// All log timestamps in this dashboard are interpreted as UTC.
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
|
||||||
|
// MMDVMHost — modem driver: log path, log filename prefix, and the
|
||||||
|
// /etc filename it reads its config from.
|
||||||
|
define('MMDVMLOGPATH', '/var/log/pi-star');
|
||||||
|
define('MMDVMLOGPREFIX', 'MMDVM');
|
||||||
|
define('MMDVMINIPATH', '/etc');
|
||||||
|
define('MMDVMINIFILENAME', 'mmdvmhost');
|
||||||
|
define('MMDVMHOSTPATH', '/usr/local/bin');
|
||||||
|
define('DMRIDDATPATH', '/usr/local/etc');
|
||||||
|
|
||||||
|
// YSFGateway (Yaesu System Fusion bridge) — same shape as MMDVM.
|
||||||
|
define('YSFGATEWAYLOGPATH', '/var/log/pi-star');
|
||||||
|
define('YSFGATEWAYLOGPREFIX', 'YSFGateway');
|
||||||
|
define('YSFGATEWAYINIPATH', '/etc');
|
||||||
|
define('YSFGATEWAYINIFILENAME', 'ysfgateway');
|
||||||
|
|
||||||
|
// P25Gateway — same shape.
|
||||||
|
define('P25GATEWAYLOGPATH', '/var/log/pi-star');
|
||||||
|
define('P25GATEWAYLOGPREFIX', 'P25Gateway');
|
||||||
|
define('P25GATEWAYINIPATH', '/etc');
|
||||||
|
define('P25GATEWAYINIFILENAME', 'p25gateway');
|
||||||
|
|
||||||
|
// ircDDBGateway — D-Star side: shared `Links.log` lives here.
|
||||||
|
define('LINKLOGPATH', '/var/log/pi-star');
|
||||||
|
define('IRCDDBGATEWAY', 'ircddbgatewayd');
|
||||||
|
|
||||||
|
// Auto-refresh hint (seconds) used by some legacy partials.
|
||||||
|
define('REFRESHAFTER', '30');
|
||||||
|
|
||||||
|
// Reserved for future use / legacy hooks. Left blank intentionally.
|
||||||
|
define('TEMPERATUREHIGHLEVEL', '');
|
||||||
|
define('REBOOTMMDVM', '');
|
||||||
|
define('REBOOTSYS', '');
|
||||||
|
define('HALTSYS', '');
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pi-Star configuration writer — safe replacement for the long-standing
|
||||||
|
* `sudo sed -i "/key=/c\\key=$value"` pattern that's repeated ~150 times
|
||||||
|
* across admin/configure.php.
|
||||||
|
*
|
||||||
|
* The previous approach concatenated `escapeshellcmd($_POST['field'])`
|
||||||
|
* into a double-quoted shell command and a sed `c\` replacement.
|
||||||
|
* `escapeshellcmd()` does not protect double-quoted shell contexts, so
|
||||||
|
* each call site was an independent command-injection sink. This helper
|
||||||
|
* replaces the family of sites with a single staged-write API: edits
|
||||||
|
* accumulate in memory, commit() writes them all at once via PHP-side
|
||||||
|
* file editing plus an atomic `sudo install`, and no shell ever sees
|
||||||
|
* an attacker-controlled byte.
|
||||||
|
*
|
||||||
|
* Usage from a POST handler:
|
||||||
|
*
|
||||||
|
* require_once $_SERVER['DOCUMENT_ROOT'] . '/config/config_writer.php';
|
||||||
|
*
|
||||||
|
* // Stage individual edits — accumulate in memory, no I/O yet.
|
||||||
|
* config_writer_stage_flat('/etc/ircddbgateway', 'aprsHostname', $aprsHost);
|
||||||
|
* config_writer_stage_flat('/etc/ircddbgateway', 'remotePassword', $confPass);
|
||||||
|
* config_writer_stage_flat('/etc/timeserver', 'callsign', $newCall);
|
||||||
|
* // ... many more ...
|
||||||
|
*
|
||||||
|
* // Commit all staged edits — one remount-rw / batched-install / remount-ro cycle.
|
||||||
|
* $errors = config_writer_commit();
|
||||||
|
* if (!empty($errors)) {
|
||||||
|
* // Each entry is a human-readable diagnostic; surface or log as you prefer.
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Design choices:
|
||||||
|
*
|
||||||
|
* - **Allowlist of writable paths.** Any caller asking to write outside
|
||||||
|
* the allowlist gets `false` back from `config_writer_stage_flat()`
|
||||||
|
* (and an `error_log()` entry). The list is the set of flat
|
||||||
|
* key=value files the dashboard's editor surfaces actually edit.
|
||||||
|
*
|
||||||
|
* - **Stage-then-commit, single mount cycle.** The dashboard runs on
|
||||||
|
* a read-only rootfs by default. A naive per-edit `remount,rw` /
|
||||||
|
* write / `remount,ro` cycle would race when many edits land in one
|
||||||
|
* POST (and configure.php has 150+ such edits per save). Staging
|
||||||
|
* all edits in `$GLOBALS['__config_writer_stage']` and flushing in a
|
||||||
|
* single `commit()` call collapses that to one mount cycle per POST.
|
||||||
|
*
|
||||||
|
* - **Match sed's `c\` semantics.** Replace the FIRST line whose head
|
||||||
|
* is `<key>=` (column 0 — stricter than sed's "anywhere on line"
|
||||||
|
* pattern, which is actually the correct intent and avoids the
|
||||||
|
* classic substring-collision class of bug). If the key is absent,
|
||||||
|
* do nothing — same as sed. Append-if-missing would be a behaviour
|
||||||
|
* change for daemons with strict parsers and isn't safe without a
|
||||||
|
* per-file audit; defer to a later opt-in flag if needed.
|
||||||
|
*
|
||||||
|
* - **Atomic file replacement via `sudo install -m 644 -o root -g root`.**
|
||||||
|
* `cp` is not atomic — open + write + close on the destination
|
||||||
|
* leaves a window where the file is partially written. `install`
|
||||||
|
* uses rename(2) when src and dst share a filesystem; falls back to
|
||||||
|
* copy+chmod+chown when they don't (which is our case here — `/tmp`
|
||||||
|
* is tmpfs, the destination is on the SD card). The fallback is no
|
||||||
|
* less atomic than the existing `cp` pattern used elsewhere in this
|
||||||
|
* dashboard, and it sets mode/owner in one go. A future hardening
|
||||||
|
* pass could co-locate the staging file with the destination for
|
||||||
|
* true rename(2) atomicity.
|
||||||
|
*
|
||||||
|
* - **Hardcoded shell command literals.** `system()` is called with a
|
||||||
|
* fixed-shape command string; only fixed paths and `escapeshellarg()`-
|
||||||
|
* wrapped arguments are interpolated. PHP's `proc_open()` array form
|
||||||
|
* is PHP 7.4+ and this codebase targets PHP 7.0+, so we stay with
|
||||||
|
* `system()` plus `escapeshellarg()` instead.
|
||||||
|
*
|
||||||
|
* - **Whitespace preservation.** Existing files use `key=value` with
|
||||||
|
* no spaces around `=`. The helper preserves that exactly — it does
|
||||||
|
* NOT normalise existing files' formatting. Daemons in this stack
|
||||||
|
* parse their own configs with byte-precise matching; introducing
|
||||||
|
* spurious whitespace changes would risk breakage out of scope for
|
||||||
|
* this fix.
|
||||||
|
*
|
||||||
|
* - **Concurrent-edits.** Pi-Star is a single-operator embedded device
|
||||||
|
* in practice. The read-mutate-write window in commit() is wider
|
||||||
|
* than sed's, but two simultaneous configure.php POSTs are vanishingly
|
||||||
|
* rare on the target hardware. TODO: add file locking via flock()
|
||||||
|
* if this assumption ever stops holding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow-list of paths the helper is permitted to write via the
|
||||||
|
* flat key=value editor. Any other path passed to
|
||||||
|
* `config_writer_stage_flat()` is rejected with an error_log()
|
||||||
|
* entry and a `false` return.
|
||||||
|
*
|
||||||
|
* Defined as a function (not a constant) so the file is safe to
|
||||||
|
* `require_once` multiple times under PHP 7.0 — array constants from
|
||||||
|
* `define()` work in 7.0 but `const` arrays at the file scope are
|
||||||
|
* 5.6+ and uniform-array-syntax limits make redefinition awkward.
|
||||||
|
*
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function config_writer_allowed_paths()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'/etc/ircddbgateway',
|
||||||
|
'/etc/dstarrepeater',
|
||||||
|
'/etc/timeserver',
|
||||||
|
'/etc/aprsgateway',
|
||||||
|
'/etc/mobilegps',
|
||||||
|
'/etc/starnetserver',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow-list of paths the helper is permitted to write via the
|
||||||
|
* privileged-flat editor (`config_writer_stage_privileged_flat()`).
|
||||||
|
*
|
||||||
|
* Same column-0 `key=value` semantics as the unprivileged flat
|
||||||
|
* editor, but the read step uses `sudo cat` so the helper can
|
||||||
|
* service files that are mode-600 root:root and therefore not
|
||||||
|
* readable by www-data. The destination is restored at mode 600
|
||||||
|
* root:root via `sudo install`. Kept separate from the flat
|
||||||
|
* allowlist because the read path and the install mode differ.
|
||||||
|
*
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function config_writer_allowed_paths_privileged_flat()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'/root/.Remote Control', // note: literal space in filename
|
||||||
|
'/etc/hostapd/hostapd.conf', // contains wpa_passphrase=… ; mode 600 root:root
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow-list of paths the helper is permitted to write via the
|
||||||
|
* PHP-statement editor (`config_writer_stage_php_string()`).
|
||||||
|
*
|
||||||
|
* These files are PHP source files included by the dashboard at
|
||||||
|
* runtime. Each contains one or more lines of the form
|
||||||
|
* `$varName='value';` at column 0. The PHP-statement editor
|
||||||
|
* rewrites the value with proper PHP-string escaping so the
|
||||||
|
* attacker-controlled bytes can never escape the single-quoted
|
||||||
|
* string literal — preventing PHP RCE via these files.
|
||||||
|
*
|
||||||
|
* Kept separate from the flat allow-list because the file shape
|
||||||
|
* and the editing semantics are different. A path that's writable
|
||||||
|
* under one editor is NOT automatically writable under the other.
|
||||||
|
*
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function config_writer_allowed_paths_php_string()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'/var/www/dashboard/config/language.php',
|
||||||
|
'/var/www/dashboard/config/ircddblocal.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage a single `key=value` edit against a flat config file.
|
||||||
|
*
|
||||||
|
* The edit is queued in process-local memory. Nothing touches disk
|
||||||
|
* until {@see config_writer_commit()} runs. Multiple stages against the
|
||||||
|
* same file accumulate; multiple stages of the same key in the same
|
||||||
|
* file overwrite each other (last write wins) and only the last is
|
||||||
|
* applied at commit time.
|
||||||
|
*
|
||||||
|
* @param string $path Absolute path. Must appear in
|
||||||
|
* {@see config_writer_allowed_paths()}.
|
||||||
|
* @param string $key The key name, e.g. `aprsHostname`. Must match
|
||||||
|
* `[A-Za-z_][A-Za-z0-9_]*` — defence in depth so
|
||||||
|
* a programming-error caller can't inject a
|
||||||
|
* regex/sed metachar via the key.
|
||||||
|
* @param string $value The new value. Must not contain NUL / CR / LF
|
||||||
|
* (those would break the line-oriented file
|
||||||
|
* format). All other bytes — including shell
|
||||||
|
* metachars like `"` `'` `;` `&` `$` — are stored
|
||||||
|
* verbatim, which is correct: the value is data,
|
||||||
|
* not a shell argument.
|
||||||
|
*
|
||||||
|
* @return bool True if staged. False if rejected (path not allow-
|
||||||
|
* listed, key malformed, or value contains NUL/CR/LF).
|
||||||
|
* On false, an error_log() entry is emitted.
|
||||||
|
*/
|
||||||
|
function config_writer_stage_flat($path, $key, $value)
|
||||||
|
{
|
||||||
|
if (!in_array($path, config_writer_allowed_paths(), true)) {
|
||||||
|
error_log("config_writer: refusing to stage edit against non-allowlisted path '$path'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!preg_match('/\A[A-Za-z_][A-Za-z0-9_]*\z/', $key)) {
|
||||||
|
error_log("config_writer: refusing to stage malformed key '$key' for $path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (preg_match('/[\x00\r\n]/', $value)) {
|
||||||
|
error_log("config_writer: refusing to stage value with NUL/CR/LF for $path:$key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage'])) {
|
||||||
|
$GLOBALS['__config_writer_stage'] = array();
|
||||||
|
}
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage'][$path])) {
|
||||||
|
$GLOBALS['__config_writer_stage'][$path] = array();
|
||||||
|
}
|
||||||
|
// Last write wins for repeated stages of the same key.
|
||||||
|
$GLOBALS['__config_writer_stage'][$path][$key] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage a single PHP single-quoted string assignment edit.
|
||||||
|
*
|
||||||
|
* Targets PHP source files in
|
||||||
|
* {@see config_writer_allowed_paths_php_string()} that contain a
|
||||||
|
* line of the form `$varName='value';` at column 0. The first
|
||||||
|
* matching line is rewritten with the new value, properly escaped
|
||||||
|
* for a PHP single-quoted string literal — `\\` and `'` inside
|
||||||
|
* the value are escaped to `\\\\` and `\\'` respectively. All
|
||||||
|
* other bytes (including shell metachars `"` `;` `&` `$`) are
|
||||||
|
* stored verbatim — they are data inside a string literal, not
|
||||||
|
* code. This closes the PHP RCE class introduced by the previous
|
||||||
|
* `sudo sed -i` pattern, where attacker bytes could close the
|
||||||
|
* sed-emitted single-quote and inject arbitrary PHP statements.
|
||||||
|
*
|
||||||
|
* Like {@see config_writer_stage_flat()}, the edit is queued in
|
||||||
|
* memory until {@see config_writer_commit()} runs.
|
||||||
|
*
|
||||||
|
* @param string $path Absolute path. Must appear in
|
||||||
|
* {@see config_writer_allowed_paths_php_string()}.
|
||||||
|
* @param string $varName PHP variable name (no leading `$`).
|
||||||
|
* Must match `[A-Za-z_][A-Za-z0-9_]*`.
|
||||||
|
* @param string $value The new value to assign. Must not contain
|
||||||
|
* NUL/CR/LF (those would either break PHP
|
||||||
|
* parsing or break the line-oriented edit).
|
||||||
|
*
|
||||||
|
* @return bool True if staged. False if rejected.
|
||||||
|
*/
|
||||||
|
function config_writer_stage_php_string($path, $varName, $value)
|
||||||
|
{
|
||||||
|
if (!in_array($path, config_writer_allowed_paths_php_string(), true)) {
|
||||||
|
error_log("config_writer: refusing PHP-string stage for non-allowlisted path '$path'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!preg_match('/\A[A-Za-z_][A-Za-z0-9_]*\z/', $varName)) {
|
||||||
|
error_log("config_writer: refusing malformed PHP var name '$varName' for $path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (preg_match('/[\x00\r\n]/', $value)) {
|
||||||
|
error_log("config_writer: refusing PHP-string value with NUL/CR/LF for $path:\$$varName");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage_phpstr'])) {
|
||||||
|
$GLOBALS['__config_writer_stage_phpstr'] = array();
|
||||||
|
}
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage_phpstr'][$path])) {
|
||||||
|
$GLOBALS['__config_writer_stage_phpstr'][$path] = array();
|
||||||
|
}
|
||||||
|
$GLOBALS['__config_writer_stage_phpstr'][$path][$varName] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage a single `key=value` edit against a flat config file that
|
||||||
|
* lives under root-only permissions.
|
||||||
|
*
|
||||||
|
* Identical contract to {@see config_writer_stage_flat()} except the
|
||||||
|
* file is read via `sudo cat` instead of the PHP-side `file()` —
|
||||||
|
* because mode-600 root:root paths are not readable by www-data —
|
||||||
|
* and the destination is reinstalled at mode 600 root:root rather
|
||||||
|
* than 644.
|
||||||
|
*
|
||||||
|
* The only currently-allowlisted path is `/root/.Remote Control`,
|
||||||
|
* which holds the ircDDBGateway remote-control password and port.
|
||||||
|
*
|
||||||
|
* @param string $path Absolute path. Must appear in
|
||||||
|
* {@see config_writer_allowed_paths_privileged_flat()}.
|
||||||
|
* @param string $key Same key contract as the unprivileged flat
|
||||||
|
* editor.
|
||||||
|
* @param string $value Same value contract.
|
||||||
|
*
|
||||||
|
* @return bool True if staged. False if rejected.
|
||||||
|
*/
|
||||||
|
function config_writer_stage_privileged_flat($path, $key, $value)
|
||||||
|
{
|
||||||
|
if (!in_array($path, config_writer_allowed_paths_privileged_flat(), true)) {
|
||||||
|
error_log("config_writer: refusing privileged-flat stage for non-allowlisted path '$path'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!preg_match('/\A[A-Za-z_][A-Za-z0-9_]*\z/', $key)) {
|
||||||
|
error_log("config_writer: refusing malformed privileged-flat key '$key' for $path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (preg_match('/[\x00\r\n]/', $value)) {
|
||||||
|
error_log("config_writer: refusing privileged-flat value with NUL/CR/LF for $path:$key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage_priv'])) {
|
||||||
|
$GLOBALS['__config_writer_stage_priv'] = array();
|
||||||
|
}
|
||||||
|
if (!isset($GLOBALS['__config_writer_stage_priv'][$path])) {
|
||||||
|
$GLOBALS['__config_writer_stage_priv'][$path] = array();
|
||||||
|
}
|
||||||
|
$GLOBALS['__config_writer_stage_priv'][$path][$key] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically install $newContent at $path with the given mode root:root.
|
||||||
|
*
|
||||||
|
* Internal helper shared by the flat and php-string commit paths.
|
||||||
|
* Returns null on success or a single-line diagnostic on failure.
|
||||||
|
* On failure the destination is left untouched and the temp file
|
||||||
|
* is unlinked.
|
||||||
|
*
|
||||||
|
* @param string $path Absolute destination path.
|
||||||
|
* @param string $newContent Full file content to install.
|
||||||
|
*
|
||||||
|
* @return string|null Error string, or null on success.
|
||||||
|
*/
|
||||||
|
function _config_writer_install_atomic($path, $newContent, $mode = '644')
|
||||||
|
{
|
||||||
|
// Defence in depth — $mode is never attacker-controlled (the
|
||||||
|
// helper's commit() picks one of two hardcoded values), but we
|
||||||
|
// refuse anything outside the small expected set so a future
|
||||||
|
// typo can't inadvertently widen file permissions.
|
||||||
|
if ($mode !== '644' && $mode !== '600') {
|
||||||
|
return "config_writer: refusing invalid mode '$mode' for $path";
|
||||||
|
}
|
||||||
|
$tmp = tempnam('/tmp', 'pistar_cw_');
|
||||||
|
if ($tmp === false) {
|
||||||
|
return "config_writer: tempnam() failed for $path";
|
||||||
|
}
|
||||||
|
// tempnam() creates the file mode 0600 on POSIX, but a non-default
|
||||||
|
// umask could in theory widen it. Force-narrow before writing —
|
||||||
|
// the temp content may be a freshly-set password.
|
||||||
|
@chmod($tmp, 0600);
|
||||||
|
if (file_put_contents($tmp, $newContent) === false) {
|
||||||
|
@unlink($tmp);
|
||||||
|
return "config_writer: file_put_contents() failed for $tmp; $path edits skipped";
|
||||||
|
}
|
||||||
|
$cmd = 'sudo install -m ' . $mode . ' -o root -g root '
|
||||||
|
. escapeshellarg($tmp) . ' '
|
||||||
|
. escapeshellarg($path);
|
||||||
|
$rc = 0;
|
||||||
|
$out = array();
|
||||||
|
exec($cmd . ' 2>&1', $out, $rc);
|
||||||
|
@unlink($tmp);
|
||||||
|
if ($rc !== 0) {
|
||||||
|
return "config_writer: install exit=$rc for $path: " . implode(' / ', $out);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply every staged edit and clear the staging buffer.
|
||||||
|
*
|
||||||
|
* For each affected file:
|
||||||
|
* 1. Read the file into a line array.
|
||||||
|
* 2. For each staged `key => value`, find the FIRST line whose head
|
||||||
|
* is `key=` (column 0) and replace it with `key=value`. If the
|
||||||
|
* key is absent, the edit is silently skipped (matches sed's
|
||||||
|
* `c\` semantics).
|
||||||
|
* 3. Write the rebuilt content to a tempnam() in /tmp.
|
||||||
|
* 4. Atomically copy back via `sudo install -m 644 -o root -g root`.
|
||||||
|
*
|
||||||
|
* Wraps the per-file install calls in a single mount-rw / mount-ro
|
||||||
|
* pair so concurrent POSTs can't race on remount toggles. Callers
|
||||||
|
* that already manage their own mount cycle (e.g. configure.php's
|
||||||
|
* top-level POST handler, which keeps `/` rw across many edits)
|
||||||
|
* should pass `$manageMount = false` to skip the helper's own
|
||||||
|
* mount-rw/ro — otherwise the helper's mount-ro will prematurely
|
||||||
|
* close the caller's write window.
|
||||||
|
*
|
||||||
|
* @param bool $manageMount Whether commit() should issue its own
|
||||||
|
* `sudo mount -o remount,rw /` and
|
||||||
|
* `... remount,ro /` around the batch.
|
||||||
|
* Default true — safe for one-off callers.
|
||||||
|
* Pass false from inside an already-managed
|
||||||
|
* mount window.
|
||||||
|
*
|
||||||
|
* @return array<int,string> Diagnostic strings — empty on full success.
|
||||||
|
* Non-empty entries describe per-file failures
|
||||||
|
* (read failure, file_put_contents failure,
|
||||||
|
* install non-zero exit). The caller decides
|
||||||
|
* whether to surface to the UI or just log.
|
||||||
|
*/
|
||||||
|
function config_writer_commit($manageMount = true)
|
||||||
|
{
|
||||||
|
$errors = array();
|
||||||
|
$flatStage = isset($GLOBALS['__config_writer_stage'])
|
||||||
|
? $GLOBALS['__config_writer_stage']
|
||||||
|
: array();
|
||||||
|
$phpStrStage = isset($GLOBALS['__config_writer_stage_phpstr'])
|
||||||
|
? $GLOBALS['__config_writer_stage_phpstr']
|
||||||
|
: array();
|
||||||
|
$privStage = isset($GLOBALS['__config_writer_stage_priv'])
|
||||||
|
? $GLOBALS['__config_writer_stage_priv']
|
||||||
|
: array();
|
||||||
|
|
||||||
|
if (empty($flatStage) && empty($phpStrStage) && empty($privStage)) {
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional single mount-rw / batched-install / mount-ro envelope.
|
||||||
|
// When the caller already has `/` open rw (configure.php's POST
|
||||||
|
// handler does this for the duration of a save), we MUST NOT do
|
||||||
|
// our own mount-ro at the end — that would prematurely close the
|
||||||
|
// caller's window and break later writes (e.g. the timezone
|
||||||
|
// handler that runs after this commit).
|
||||||
|
if ($manageMount) {
|
||||||
|
system('sudo mount -o remount,rw /');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1 — flat key=value edits.
|
||||||
|
foreach ($flatStage as $path => $kvPairs) {
|
||||||
|
if (!is_readable($path)) {
|
||||||
|
$errors[] = "config_writer: cannot read $path; edits skipped";
|
||||||
|
error_log("config_writer: cannot read $path; " . count($kvPairs) . " edits skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||||
|
if ($lines === false) {
|
||||||
|
$errors[] = "config_writer: file() failed for $path; edits skipped";
|
||||||
|
error_log("config_writer: file() failed for $path; " . count($kvPairs) . " edits skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($kvPairs as $key => $value) {
|
||||||
|
$prefix = $key . '=';
|
||||||
|
$applied = false;
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
if (strpos($line, $prefix) === 0) {
|
||||||
|
$lines[$i] = $prefix . $value;
|
||||||
|
$applied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Silent no-op when the key isn't present — matches the
|
||||||
|
// previous sed `c\` semantics. If the caller cares, log
|
||||||
|
// here. (Quiet by default to avoid filling the log on
|
||||||
|
// version-skew between the dashboard and the gateway
|
||||||
|
// configs.)
|
||||||
|
if (!$applied) {
|
||||||
|
error_log("config_writer: $path has no '$key=' line; edit skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$err = _config_writer_install_atomic(
|
||||||
|
$path,
|
||||||
|
implode("\n", $lines) . "\n"
|
||||||
|
);
|
||||||
|
if ($err !== null) {
|
||||||
|
$errors[] = $err;
|
||||||
|
error_log($err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — PHP single-quoted string assignment edits.
|
||||||
|
foreach ($phpStrStage as $path => $varValuePairs) {
|
||||||
|
if (!is_readable($path)) {
|
||||||
|
$errors[] = "config_writer: cannot read $path; PHP-string edits skipped";
|
||||||
|
error_log("config_writer: cannot read $path; "
|
||||||
|
. count($varValuePairs) . " PHP-string edits skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||||
|
if ($lines === false) {
|
||||||
|
$errors[] = "config_writer: file() failed for $path; PHP-string edits skipped";
|
||||||
|
error_log("config_writer: file() failed for $path; "
|
||||||
|
. count($varValuePairs) . " PHP-string edits skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($varValuePairs as $varName => $value) {
|
||||||
|
// Match column-0 `$varName` followed by optional whitespace
|
||||||
|
// then `=`. preg_quote is belt-and-braces — the var name is
|
||||||
|
// already validated to /^[A-Za-z_][A-Za-z0-9_]*$/ by
|
||||||
|
// config_writer_stage_php_string().
|
||||||
|
$pattern = '/^\$' . preg_quote($varName, '/') . '\s*=/';
|
||||||
|
$applied = false;
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
if (preg_match($pattern, $line)) {
|
||||||
|
// Escape value for a PHP single-quoted string
|
||||||
|
// literal: only `\` and `'` are special inside
|
||||||
|
// `'...'`. The two-char mask "\\'" tells
|
||||||
|
// addcslashes() to backslash-escape both:
|
||||||
|
// `\` → `\\` (first char of the mask)
|
||||||
|
// `'` → `\'` (second char of the mask)
|
||||||
|
// The result is parseable PHP that decodes back
|
||||||
|
// to the original $value bytes — so attacker
|
||||||
|
// bytes are stored as data, never executed.
|
||||||
|
$escaped = addcslashes($value, "\\'");
|
||||||
|
$lines[$i] = '$' . $varName . "='" . $escaped . "';";
|
||||||
|
$applied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$applied) {
|
||||||
|
error_log("config_writer: $path has no \$$varName= line; PHP-string edit skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$err = _config_writer_install_atomic(
|
||||||
|
$path,
|
||||||
|
implode("\n", $lines) . "\n"
|
||||||
|
);
|
||||||
|
if ($err !== null) {
|
||||||
|
$errors[] = $err;
|
||||||
|
error_log($err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 3 — privileged flat key=value edits (root-only files).
|
||||||
|
// Same column-0 `key=` semantics as pass 1, but the read step
|
||||||
|
// uses `sudo cat` so we can service mode-600 root:root paths,
|
||||||
|
// and the install mode is 600 not 644.
|
||||||
|
foreach ($privStage as $path => $kvPairs) {
|
||||||
|
$rc = 0;
|
||||||
|
$out = array();
|
||||||
|
// sudo -n: never prompt — fail loudly if sudoers ever changes.
|
||||||
|
// Output is the file content; stderr captured separately so a
|
||||||
|
// sudo / cat failure doesn't poison the parsed lines.
|
||||||
|
exec('sudo -n cat ' . escapeshellarg($path) . ' 2>/dev/null',
|
||||||
|
$out, $rc);
|
||||||
|
if ($rc !== 0) {
|
||||||
|
$errors[] = "config_writer: sudo cat exit=$rc for $path; privileged edits skipped";
|
||||||
|
error_log("config_writer: sudo cat exit=$rc for $path; "
|
||||||
|
. count($kvPairs) . " privileged edits skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lines = $out;
|
||||||
|
|
||||||
|
foreach ($kvPairs as $key => $value) {
|
||||||
|
$prefix = $key . '=';
|
||||||
|
$applied = false;
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
if (strpos($line, $prefix) === 0) {
|
||||||
|
$lines[$i] = $prefix . $value;
|
||||||
|
$applied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$applied) {
|
||||||
|
error_log("config_writer: $path has no '$key=' line; privileged edit skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$err = _config_writer_install_atomic(
|
||||||
|
$path,
|
||||||
|
implode("\n", $lines) . "\n",
|
||||||
|
'600'
|
||||||
|
);
|
||||||
|
if ($err !== null) {
|
||||||
|
$errors[] = $err;
|
||||||
|
error_log($err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manageMount) {
|
||||||
|
system('sudo mount -o remount,ro /');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all stages so a subsequent commit() call doesn't
|
||||||
|
// re-apply already-applied edits.
|
||||||
|
$GLOBALS['__config_writer_stage'] = array();
|
||||||
|
$GLOBALS['__config_writer_stage_phpstr'] = array();
|
||||||
|
$GLOBALS['__config_writer_stage_priv'] = array();
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CSRF (Cross-Site Request Forgery) protection primitive for the
|
||||||
|
* Pi-Star dashboard.
|
||||||
|
*
|
||||||
|
* Threat model
|
||||||
|
* ============
|
||||||
|
*
|
||||||
|
* The dashboard sits behind Apache HTTP Basic Auth on a LAN. The
|
||||||
|
* authenticated administrator's browser will automatically attach
|
||||||
|
* the basic-auth credential to ANY request the browser sends to the
|
||||||
|
* dashboard's host — including requests triggered by a malicious
|
||||||
|
* page open in another tab. Without CSRF protection, that hostile
|
||||||
|
* page can POST to e.g. `/admin/power.php` and reboot the device,
|
||||||
|
* or POST a full configuration to `/admin/configure.php`, simply
|
||||||
|
* because the user's browser is sitting on cached basic-auth.
|
||||||
|
*
|
||||||
|
* The mitigation: every state-changing POST handler MUST verify
|
||||||
|
* that the request carries a server-issued, session-scoped token
|
||||||
|
* the attacker cannot read (same-origin policy prevents the hostile
|
||||||
|
* page from reading the dashboard's HTML to extract it).
|
||||||
|
*
|
||||||
|
* Public API
|
||||||
|
* ==========
|
||||||
|
*
|
||||||
|
* csrf_token() -> string (64 hex chars; idempotent within a session)
|
||||||
|
* csrf_field() -> echoes a hidden <input> tag for forms
|
||||||
|
* csrf_verify() -> dies with HTTP 403 if the POSTed token is missing
|
||||||
|
* or invalid; returns silently otherwise
|
||||||
|
*
|
||||||
|
* Usage
|
||||||
|
* =====
|
||||||
|
*
|
||||||
|
* In a top-level GET-rendered page that contains a POST form:
|
||||||
|
*
|
||||||
|
* require_once $_SERVER['DOCUMENT_ROOT'] . '/config/csrf.php';
|
||||||
|
* ...
|
||||||
|
* <form method="post" action="...">
|
||||||
|
* <?php csrf_field(); ?>
|
||||||
|
* ...
|
||||||
|
* </form>
|
||||||
|
*
|
||||||
|
* In the matching POST handler (top of the file, before any state
|
||||||
|
* change):
|
||||||
|
*
|
||||||
|
* require_once $_SERVER['DOCUMENT_ROOT'] . '/config/csrf.php';
|
||||||
|
* if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
* csrf_verify();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Design notes
|
||||||
|
* ============
|
||||||
|
*
|
||||||
|
* - One token per session, reused across forms. Simpler retrofit
|
||||||
|
* than per-form tokens; no UX downside (the attacker still can't
|
||||||
|
* read the token, which is what matters). The token rotates
|
||||||
|
* when the session itself rotates (typically: browser closed,
|
||||||
|
* basic-auth re-prompted, or `session_regenerate_id()` called
|
||||||
|
* elsewhere).
|
||||||
|
*
|
||||||
|
* - Token = `bin2hex(random_bytes(32))` -> 64 hex chars (256 bits
|
||||||
|
* of entropy). `random_bytes()` is PHP 7.0+ and uses the
|
||||||
|
* OS CSPRNG (`/dev/urandom` on Linux). PHP 7.0 is this codebase's
|
||||||
|
* stated floor, so no fallback needed.
|
||||||
|
*
|
||||||
|
* - Verification uses `hash_equals()` (PHP 5.6+) for constant-
|
||||||
|
* time comparison. This is overkill for a hex-vs-hex comparison
|
||||||
|
* against a 256-bit secret, but costs nothing and removes any
|
||||||
|
* theoretical timing-leak class.
|
||||||
|
*
|
||||||
|
* - Failure mode (HTTP 403): write a minimal, self-contained HTML
|
||||||
|
* page rather than a JSON blob. The dashboard's forms post
|
||||||
|
* directly and the user lands on the response body in their
|
||||||
|
* browser — they need to understand what happened.
|
||||||
|
*
|
||||||
|
* - We deliberately do NOT call session_regenerate_id() on each
|
||||||
|
* request. Several existing pages (admin/update.php,
|
||||||
|
* admin/calibration.php, admin/live_modem_log.php) store
|
||||||
|
* log-tail offsets in $_SESSION across many AJAX requests; a
|
||||||
|
* mid-session ID rotation would lose those offsets and break
|
||||||
|
* the live log tails.
|
||||||
|
*
|
||||||
|
* - GET requests are NOT verified. CSRF protection only applies
|
||||||
|
* to state-changing requests, and the dashboard's idempotent
|
||||||
|
* read pages are POST-free by convention.
|
||||||
|
*
|
||||||
|
* - Session-cookie hardening. We set HttpOnly + SameSite=Lax on
|
||||||
|
* every issued PHPSESSID, and Secure conditionally when the
|
||||||
|
* request is HTTPS (via isHttps() in security_headers.php —
|
||||||
|
* covers direct TLS to nginx AND reverse-proxy / Cloudflare
|
||||||
|
* terminations that forward via X-Forwarded-Proto). See
|
||||||
|
* {@see _csrf_set_cookie_params()}.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the PHPSESSID cookie's flags for hardened delivery.
|
||||||
|
*
|
||||||
|
* Must be called BEFORE session_start(); has no effect once the
|
||||||
|
* session is active. Sets:
|
||||||
|
*
|
||||||
|
* HttpOnly — always. Blocks document.cookie access from JS, so
|
||||||
|
* a future XSS in any rendered page can't read the
|
||||||
|
* session ID. Defence in depth alongside the input
|
||||||
|
* escaping work in the rest of the security pass.
|
||||||
|
*
|
||||||
|
* SameSite=Lax — always. Browser stops sending the cookie on
|
||||||
|
* cross-site subresource fetches and cross-site POSTs;
|
||||||
|
* top-level navigation (clicking a bookmark, following
|
||||||
|
* a same-origin redirect) still carries it, so UX
|
||||||
|
* doesn't change. Belt-and-braces with the existing
|
||||||
|
* CSRF-token check.
|
||||||
|
*
|
||||||
|
* Secure — conditional on isHttps(). UNCONDITIONALLY setting
|
||||||
|
* Secure would invalidate the cookie for the (large)
|
||||||
|
* population of operators on plain-HTTP LAN access
|
||||||
|
* and silently break CSRF protection. isHttps() also
|
||||||
|
* returns true for X-Forwarded-Proto: https — so
|
||||||
|
* operators behind Cloudflare / a reverse proxy /
|
||||||
|
* Tailscale Funnel get the right Secure flag even
|
||||||
|
* though nginx itself only sees plain HTTP.
|
||||||
|
*
|
||||||
|
* Trust caveat: an attacker who can talk directly
|
||||||
|
* to nginx on port 80 (bypassing the proxy) could
|
||||||
|
* spoof X-Forwarded-Proto and trick the dashboard
|
||||||
|
* into setting Secure on their own session. The
|
||||||
|
* consequence is the spoofer's cookie won't replay
|
||||||
|
* over plain HTTP — a self-DoS, not an escalation.
|
||||||
|
*
|
||||||
|
* PHP version handling. The samesite option was added to
|
||||||
|
* session_set_cookie_params() in PHP 7.3 (the array form). On
|
||||||
|
* 7.0..7.2 we fall back to the well-known path-suffix kludge:
|
||||||
|
* appending `; SameSite=Lax` to the path argument. PHP doesn't
|
||||||
|
* validate the path string — it concatenates verbatim into the
|
||||||
|
* Set-Cookie header — and browsers parse `path=/; SameSite=Lax`
|
||||||
|
* correctly because `;` terminates the path attribute. The
|
||||||
|
* codebase floor is PHP 7.0; the production runtime is PHP 8.2.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function _csrf_set_cookie_params()
|
||||||
|
{
|
||||||
|
$secure = function_exists('isHttps') ? isHttps() : false;
|
||||||
|
if (PHP_VERSION_ID >= 70300) {
|
||||||
|
// Modern array form. Available since PHP 7.3.
|
||||||
|
@session_set_cookie_params(array(
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '',
|
||||||
|
'secure' => $secure,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// PHP 7.0..7.2: no native samesite support. The path-suffix
|
||||||
|
// kludge is the documented workaround — see PHP RFC for
|
||||||
|
// 7.3's array form, where this is acknowledged as the
|
||||||
|
// pre-7.3 idiom. lifetime/path/domain/secure/httponly here
|
||||||
|
// mirror the array values above.
|
||||||
|
@session_set_cookie_params(0, '/; SameSite=Lax', '', $secure, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a session is started. Idempotent.
|
||||||
|
*
|
||||||
|
* Several dashboard pages already call `session_start()` for their
|
||||||
|
* own state (log offsets, etc.), so this primitive must coexist
|
||||||
|
* gracefully with prior `session_start()` calls. PHP_SESSION_ACTIVE
|
||||||
|
* is the canonical guard introduced in PHP 5.4.
|
||||||
|
*/
|
||||||
|
function csrf_session_start()
|
||||||
|
{
|
||||||
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
// Pi-Star ships with `session.gc_probability=0` AND
|
||||||
|
// /var/lib/php/sessions mounted as a 64KB tmpfs (per
|
||||||
|
// /etc/fstab in the OS image). With CSRF, every dashboard
|
||||||
|
// visit creates a session file, and without automatic GC
|
||||||
|
// those accumulate until reboot — at which point the tmpfs
|
||||||
|
// fills (~15 sessions) and session_start() starts failing
|
||||||
|
// with "No space left on device", silently breaking CSRF.
|
||||||
|
//
|
||||||
|
// Force GC at session-start time by bumping the probability
|
||||||
|
// ratio to 1/1. PHP runs GC itself during session_start when
|
||||||
|
// (gc_probability / gc_divisor) > random — at 1/1 that's
|
||||||
|
// every call, but only for THIS request's session_start.
|
||||||
|
// Cost: a tmpfs `glob` + a few `unlink`s, microseconds.
|
||||||
|
// The @-suppression handles hosts that disallow ini_set on
|
||||||
|
// these keys; failure just means we fall back to PHP's
|
||||||
|
// default behaviour, same as before this fix.
|
||||||
|
//
|
||||||
|
// session_gc() (PHP 7.1+) would be cleaner but the codebase
|
||||||
|
// targets PHP 7.0. ini_set works on every supported version.
|
||||||
|
if ((int)ini_get('session.gc_probability') === 0) {
|
||||||
|
@ini_set('session.gc_probability', '1');
|
||||||
|
@ini_set('session.gc_divisor', '1');
|
||||||
|
}
|
||||||
|
// gc_maxlifetime is intentionally NOT overridden here — we
|
||||||
|
// defer to Pi-Star's stock /etc/php/*/fpm/php.ini value
|
||||||
|
// (1440 s, matching PHP's own default). The dashboard's
|
||||||
|
// AJAX-refreshing panels (lh.php, repeaterinfo.php, the
|
||||||
|
// bm_links / tgif_links partials, etc.) do not load csrf.php,
|
||||||
|
// so they don't update the session file's mtime — meaning the
|
||||||
|
// session counts as "idle" from the moment csrf_verify() last
|
||||||
|
// ran on a top-level page load, even while the dashboard is
|
||||||
|
// visibly active in the operator's tab. Anything shorter than
|
||||||
|
// ~24 min would routinely 403 BM-manager / TGIF-manager /
|
||||||
|
// configure.php POSTs whenever the operator left the dashboard
|
||||||
|
// tab open between page loads. tmpfs containment is the
|
||||||
|
// pre-emptive prune below, not maxlifetime.
|
||||||
|
//
|
||||||
|
// Pi-Star's /var/lib/php/sessions tmpfs is sized 64 KB (per
|
||||||
|
// /etc/fstab in the OS image) — about 15 session files at
|
||||||
|
// a 4 KB tmpfs block each. csrf.php is only loaded behind
|
||||||
|
// basic auth on /admin/*, so the only session-creators are
|
||||||
|
// authenticated operators (whose browsers reuse one cookie =
|
||||||
|
// one session) and tooling that hits the dashboard with
|
||||||
|
// fresh cookie jars per request. The latter has filled the
|
||||||
|
// tmpfs in practice; once full, session_start() fails with
|
||||||
|
// "No space left on device" and CSRF silently breaks for
|
||||||
|
// the operator — they get a 403 on the next form submit
|
||||||
|
// because $_SESSION['csrf_token'] could not be persisted.
|
||||||
|
//
|
||||||
|
// Belt-and-braces safety net: cap the session directory at
|
||||||
|
// 12 files BEFORE session_start() tries to write a new one.
|
||||||
|
// GC alone won't help here because a burst of fresh-cookie
|
||||||
|
// requests can fill the 64 KB tmpfs faster than gc_maxlifetime
|
||||||
|
// expires anything. This pre-emptive prune deletes the
|
||||||
|
// oldest sess_* files until 12 remain, leaving ~3 slots of
|
||||||
|
// headroom under the ~15-file tmpfs cap.
|
||||||
|
//
|
||||||
|
// Best-effort: any failure here (permissions, missing dir,
|
||||||
|
// glob/unlink errors) is silently ignored — session_start()
|
||||||
|
// will still try and either succeed or fall through to the
|
||||||
|
// existing failure-logging path below. Worst case for an
|
||||||
|
// evicted session is an operator gets a 403 on the next
|
||||||
|
// form submit and a page reload re-issues a token, which
|
||||||
|
// is far better than the disk-full failure this guards
|
||||||
|
// against.
|
||||||
|
$sessSaveDir = (string)@ini_get('session.save_path');
|
||||||
|
if ($sessSaveDir !== '') {
|
||||||
|
$sessFiles = @glob($sessSaveDir . '/sess_*');
|
||||||
|
if (is_array($sessFiles) && count($sessFiles) > 12) {
|
||||||
|
usort($sessFiles, function ($a, $b) {
|
||||||
|
return (int)@filemtime($a) - (int)@filemtime($b);
|
||||||
|
});
|
||||||
|
$sessExcess = count($sessFiles) - 12;
|
||||||
|
for ($i = 0; $i < $sessExcess; $i++) {
|
||||||
|
@unlink($sessFiles[$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Harden the PHPSESSID cookie flags BEFORE session_start()
|
||||||
|
// emits the Set-Cookie header. See _csrf_set_cookie_params()
|
||||||
|
// for the per-flag rationale.
|
||||||
|
_csrf_set_cookie_params();
|
||||||
|
// Suppress notices about headers already sent — some
|
||||||
|
// dashboard pages emit output before this is reached.
|
||||||
|
// The session won't be usable in that scenario, but
|
||||||
|
// failing closed (csrf_verify rejects the POST) is the
|
||||||
|
// correct outcome. Log the underlying cause so a future
|
||||||
|
// maintainer who accidentally reorders requires above an
|
||||||
|
// echo can see why their POSTs started 403'ing.
|
||||||
|
if (@session_start() === false) {
|
||||||
|
error_log('csrf_session_start: session_start() failed '
|
||||||
|
. '(headers already sent? require_once order?)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the session's CSRF token, issuing a fresh one on first
|
||||||
|
* call within a session.
|
||||||
|
*
|
||||||
|
* @return string 64-character hex string.
|
||||||
|
*/
|
||||||
|
function csrf_token()
|
||||||
|
{
|
||||||
|
csrf_session_start();
|
||||||
|
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo a hidden form input carrying the CSRF token.
|
||||||
|
*
|
||||||
|
* Place INSIDE the `<form>` element, before the submit button.
|
||||||
|
* Output is htmlspecialchars-safe: a hex string never contains
|
||||||
|
* any character that needs escaping, but we encode anyway as a
|
||||||
|
* defence against future changes to the token format.
|
||||||
|
*
|
||||||
|
* For sites that build form HTML into a string variable rather
|
||||||
|
* than echoing inline, see {@see csrf_field_html()}.
|
||||||
|
*/
|
||||||
|
function csrf_field()
|
||||||
|
{
|
||||||
|
echo csrf_field_html();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a hidden form input carrying the CSRF token, as a
|
||||||
|
* string. Useful for code that accumulates form HTML into a
|
||||||
|
* `$output` variable (e.g. wifi.php's wpa_conf form) where an
|
||||||
|
* `echo` mid-expression doesn't compose.
|
||||||
|
*
|
||||||
|
* @return string The hidden-input HTML.
|
||||||
|
*/
|
||||||
|
function csrf_field_html()
|
||||||
|
{
|
||||||
|
$tok = htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8');
|
||||||
|
return '<input type="hidden" name="csrf_token" value="' . $tok . '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the POSTed CSRF token. On mismatch, emit HTTP 403 and exit.
|
||||||
|
*
|
||||||
|
* Call this from EVERY state-changing POST handler before any side
|
||||||
|
* effect (file write, system call, session mutation, etc.). It is
|
||||||
|
* a no-op for GET / HEAD / OPTIONS requests — those are read-only
|
||||||
|
* by convention in this dashboard.
|
||||||
|
*
|
||||||
|
* The function does not return on failure: it sets the response
|
||||||
|
* code, prints a minimal HTML error page, and calls exit().
|
||||||
|
*/
|
||||||
|
function csrf_verify()
|
||||||
|
{
|
||||||
|
// Bootstrap the session up front, even on GET, so the
|
||||||
|
// Set-Cookie header gets emitted before any HTML output.
|
||||||
|
// csrf_field() (called inside the page's <form> tags) is
|
||||||
|
// lazy and may not run until well after output has started,
|
||||||
|
// so without an early csrf_verify() call sites that don't
|
||||||
|
// already have their own pre-output session_start() (most
|
||||||
|
// pages — power.php is the exception) never get a session
|
||||||
|
// cookie. Without a cookie the GET-issued token has no way
|
||||||
|
// to reach the POST handler.
|
||||||
|
//
|
||||||
|
// Pages should call csrf_verify() near the top of the file,
|
||||||
|
// BEFORE any output. On GET it bootstraps the session and
|
||||||
|
// returns; on POST it bootstraps, validates, and either
|
||||||
|
// returns silently or emits 403 + exit().
|
||||||
|
csrf_session_start();
|
||||||
|
|
||||||
|
if (!isset($_SERVER['REQUEST_METHOD']) ||
|
||||||
|
$_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
// Only POST is gated — GET pages render the token via
|
||||||
|
// csrf_field() and don't need verification.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = isset($_SESSION['csrf_token']) ? $_SESSION['csrf_token'] : '';
|
||||||
|
$supplied = isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '';
|
||||||
|
|
||||||
|
// Distinguish "session has no token" from "session has a token
|
||||||
|
// and the supplied one is wrong". The former is almost always a
|
||||||
|
// benign expired-session POST (operator left the dashboard tab
|
||||||
|
// open past gc_maxlifetime, then clicked submit) and shouldn't
|
||||||
|
// throw the alarmist 403 page at them. Redirect 303 to the same
|
||||||
|
// URL — the browser switches to GET, the page re-renders fresh
|
||||||
|
// (forms that pre-populate from disk come back filled in; the
|
||||||
|
// session_start() in csrf_session_start() issues a new token),
|
||||||
|
// and the operator's submit retry just works.
|
||||||
|
//
|
||||||
|
// This is safe against forgery because a real attacker would be
|
||||||
|
// sending against an ACTIVE operator session whose
|
||||||
|
// $_SESSION['csrf_token'] is non-empty — that path stays on the
|
||||||
|
// strict 403 below. The only thing the redirect "lets through"
|
||||||
|
// is a fresh form render, which any GET would also serve.
|
||||||
|
if ($expected === '' || !is_string($expected) || strlen($expected) !== 64) {
|
||||||
|
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/admin/';
|
||||||
|
// Defence in depth: REQUEST_URI is typically same-origin
|
||||||
|
// already, but force a relative path so a crafted Host or
|
||||||
|
// proxy can't turn this into an open redirect.
|
||||||
|
$uri = '/' . ltrim(preg_replace('#^https?://[^/]*#i', '', $uri), '/');
|
||||||
|
header('Location: ' . $uri, true, 303);
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject only when the session actually had a token but the
|
||||||
|
// supplied one is missing or doesn't match — that IS a forgery.
|
||||||
|
if (!is_string($supplied) || strlen($supplied) !== 64 ||
|
||||||
|
!hash_equals($expected, $supplied)) {
|
||||||
|
// Best-effort log line for the operator. The remote IP is
|
||||||
|
// typically a LAN address but worth recording in case a
|
||||||
|
// rogue device is fingerprinted by repeated 403s.
|
||||||
|
$remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '?';
|
||||||
|
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '?';
|
||||||
|
error_log("csrf_verify: rejected POST from $remote to $uri");
|
||||||
|
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
// Don't pollute browser history with this response.
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
// English-only by design: the dashboard's lang/ system
|
||||||
|
// requires config/language.php, which reads /etc/pistar-release
|
||||||
|
// and the gateway configs. Pulling that whole stack into an
|
||||||
|
// error path that only fires under attack is disproportionate.
|
||||||
|
echo '<!DOCTYPE html><html lang="en"><head>'
|
||||||
|
. '<meta charset="utf-8" /><title>403 Forbidden</title></head>'
|
||||||
|
. '<body><h1>403 Forbidden</h1>'
|
||||||
|
. '<p>This request did not include a valid CSRF token. '
|
||||||
|
. 'If you reached this page by clicking a link from another site, '
|
||||||
|
. 'that other site may have been trying to perform an action on '
|
||||||
|
. 'your behalf without your consent.</p>'
|
||||||
|
. '<p>If you reached this page by submitting a form on the '
|
||||||
|
. 'dashboard, your session may have expired. Reload the page '
|
||||||
|
. 'and try again.</p>'
|
||||||
|
. '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token verified — strip it from $_POST so downstream handlers
|
||||||
|
// that iterate $_POST (e.g. fulledit_bmapikey.php and
|
||||||
|
// fulledit_dapnetapi.php's INI writers, which treat each top-
|
||||||
|
// level POST key as an [INI section]) don't accidentally write
|
||||||
|
// a stray `[csrf token]` block into /etc/<file>. Without this,
|
||||||
|
// every successful submit on those editors prepended an empty
|
||||||
|
// `[csrf token]` section to the saved config and rendered a
|
||||||
|
// ghost table titled "csrf_token" on the response page.
|
||||||
|
// Centralising the unset here means every current and future
|
||||||
|
// POST handler is immune without needing to remember the dance.
|
||||||
|
unset($_POST['csrf_token']);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# ircddbgateway_languages.inc
|
||||||
|
#
|
||||||
|
# Written for Pi-Star Digital Voice by Andy Taylor (MW0MWZ)
|
||||||
|
# Orrigional request by Dennis Bonesky
|
||||||
|
#
|
||||||
|
# Updated 28-May-2017
|
||||||
|
#
|
||||||
|
###########################################################
|
||||||
|
#language;ircddbgateway;timeserver
|
||||||
|
###########################################################
|
||||||
|
English_(UK);0;0
|
||||||
|
Deutsch;1;4
|
||||||
|
Dansk;2;0
|
||||||
|
Francais;3;6
|
||||||
|
Italiano;4;0
|
||||||
|
Polski;5;0
|
||||||
|
English_(US);6;2
|
||||||
|
Espanol;7;9
|
||||||
|
Svenska;8;8
|
||||||
|
Nederlands_(NL);9;7
|
||||||
|
Nederlands_(BE);10;7
|
||||||
|
Norsk;11;10
|
||||||
|
Portugues;12;11
|
||||||
|
###########################################################
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Local paths and identity for ircDDBGateway-related dashboard code.
|
||||||
|
*
|
||||||
|
* Defines where the gateway's logs and config files live on the device,
|
||||||
|
* plus the local callsign placeholder. The dashboard reads these paths
|
||||||
|
* to render the D-Star and CCS panels and to back the configure.php
|
||||||
|
* editor. `configure.php` rewrites `$callsign=` here when the operator
|
||||||
|
* changes their callsign.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Log directory and the four ircDDBGateway-managed log files.
|
||||||
|
$logPath = '/var/log/pi-star';
|
||||||
|
$starLogPath = $logPath . '/STARnet.log';
|
||||||
|
$linkLogPath = $logPath . '/Links.log';
|
||||||
|
$hdrLogPath = $logPath . '/Headers.log';
|
||||||
|
$ddmode_log = $logPath . '/DDMode.log';
|
||||||
|
|
||||||
|
// Local node identity (rewritten by configure.php).
|
||||||
|
$callsign = 'M1ABC';
|
||||||
|
$registerURL = '';
|
||||||
|
|
||||||
|
// On-disk locations the dashboard reads/writes.
|
||||||
|
$configPath = '/etc';
|
||||||
|
$gatewayConfigPath = '/etc/ircddbgateway';
|
||||||
|
$defaultConfPath = '/etc/default';
|
||||||
|
$sharedFilesPath = '/usr/local/etc';
|
||||||
|
$sysConfigPath = '/etc/sysconfig';
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
// Set the language
|
||||||
|
$pistarLanguage='chinese_cn';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT']."/lang/$pistarLanguage.php";
|
||||||
|
?>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Security headers for the Pi-Star Dashboard.
|
||||||
|
*
|
||||||
|
* Centralises Content-Security-Policy, X-Frame-Options, X-Content-Type-Options,
|
||||||
|
* X-XSS-Protection, Referrer-Policy, Permissions-Policy, and HSTS so every
|
||||||
|
* dashboard entry-point sets the same baseline.
|
||||||
|
*
|
||||||
|
* Three flavours are provided because dashboard pages fall into three
|
||||||
|
* distinct categories:
|
||||||
|
*
|
||||||
|
* - {@see setSecurityHeaders()} — top-level pages
|
||||||
|
* (index, admin, configure, editors). Locks frame ancestors and frame-src
|
||||||
|
* to same-origin to prevent the page being framed by a hostile site.
|
||||||
|
*
|
||||||
|
* - {@see setEmbeddableSecurityHeaders()} — AJAX-loaded partials
|
||||||
|
* (last-heard list, local TX, mode info, etc.). Same CSP minus the frame
|
||||||
|
* restrictions, because the partial is itself loaded into a parent page
|
||||||
|
* via $.load() and would otherwise refuse to embed.
|
||||||
|
*
|
||||||
|
* - {@see setSecurityHeadersAllowDifferentPorts()} — pages that iframe
|
||||||
|
* services on different ports of the same host (e.g. shellinabox SSH on
|
||||||
|
* a non-80 port). Adds frame-src for the same hostname on any port.
|
||||||
|
*
|
||||||
|
* HSTS is only emitted when the request is over HTTPS (direct or via an
|
||||||
|
* upstream proxy reporting X-Forwarded-Proto). All three functions are
|
||||||
|
* idempotent and bail early if headers have already been sent.
|
||||||
|
*
|
||||||
|
* The CSP intentionally allows 'unsafe-inline' for both scripts and styles
|
||||||
|
* because the dashboard inlines large amounts of JS and inline `style="…"`
|
||||||
|
* attributes; tightening this would require a much larger refactor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether the current request is being served over HTTPS.
|
||||||
|
*
|
||||||
|
* Checks both direct indicators ($_SERVER['HTTPS'], port 443) and proxy
|
||||||
|
* headers (X-Forwarded-Proto, X-Forwarded-SSL) so we still detect HTTPS
|
||||||
|
* correctly when sitting behind a TLS-terminating proxy.
|
||||||
|
*
|
||||||
|
* @return bool True if the request is over HTTPS, false otherwise.
|
||||||
|
*/
|
||||||
|
function isHttps() {
|
||||||
|
// Check standard HTTPS indicators
|
||||||
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for proxy/load balancer headers
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set full security headers for non-embeddable pages
|
||||||
|
* Use for: Admin pages, configuration editors, main entry points
|
||||||
|
*/
|
||||||
|
function setSecurityHeaders() {
|
||||||
|
// Only set headers if they haven't been sent yet
|
||||||
|
if (!headers_sent()) {
|
||||||
|
$isHttps = isHttps();
|
||||||
|
|
||||||
|
header("X-Frame-Options: SAMEORIGIN");
|
||||||
|
header("X-Content-Type-Options: nosniff");
|
||||||
|
header("X-XSS-Protection: 1; mode=block");
|
||||||
|
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||||
|
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||||
|
|
||||||
|
// Build CSP based on protocol
|
||||||
|
// Allow external images via both http: and https: since we can't control external links
|
||||||
|
$imgSrc = $isHttps ? "'self' data: https:" : "'self' data: http: https:";
|
||||||
|
|
||||||
|
$csp = "default-src 'self'; " .
|
||||||
|
"script-src 'self' 'unsafe-inline'; " .
|
||||||
|
"style-src 'self' 'unsafe-inline'; " .
|
||||||
|
"img-src {$imgSrc}; " .
|
||||||
|
"connect-src 'self'; " .
|
||||||
|
"frame-ancestors 'self'";
|
||||||
|
|
||||||
|
header("Content-Security-Policy: " . $csp);
|
||||||
|
|
||||||
|
// Only add HSTS if served over HTTPS
|
||||||
|
if ($isHttps) {
|
||||||
|
// HSTS: Force HTTPS for 1 year, but don't include subdomains (might be on local network)
|
||||||
|
header("Strict-Transport-Security: max-age=31536000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set embeddable security headers for display components
|
||||||
|
* Use for: Status displays, last heard lists, info panels meant to be embeddable
|
||||||
|
*/
|
||||||
|
function setEmbeddableSecurityHeaders() {
|
||||||
|
// Only set headers if they haven't been sent yet
|
||||||
|
if (!headers_sent()) {
|
||||||
|
$isHttps = isHttps();
|
||||||
|
|
||||||
|
// Note: X-Frame-Options omitted to allow embedding
|
||||||
|
header("X-Content-Type-Options: nosniff");
|
||||||
|
header("X-XSS-Protection: 1; mode=block");
|
||||||
|
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||||
|
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||||
|
|
||||||
|
// Build CSP based on protocol
|
||||||
|
$imgSrc = $isHttps ? "'self' data: https:" : "'self' data: http: https:";
|
||||||
|
|
||||||
|
$csp = "default-src 'self'; " .
|
||||||
|
"script-src 'self' 'unsafe-inline'; " .
|
||||||
|
"style-src 'self' 'unsafe-inline'; " .
|
||||||
|
"img-src {$imgSrc}; " .
|
||||||
|
"connect-src 'self'";
|
||||||
|
|
||||||
|
header("Content-Security-Policy: " . $csp);
|
||||||
|
|
||||||
|
// Only add HSTS if served over HTTPS
|
||||||
|
if ($isHttps) {
|
||||||
|
header("Strict-Transport-Security: max-age=31536000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set security headers for pages that embed content from different ports on same host
|
||||||
|
*
|
||||||
|
* This allows iframes from the same hostname but different ports
|
||||||
|
*/
|
||||||
|
function setSecurityHeadersAllowDifferentPorts() {
|
||||||
|
// Only set headers if they haven't been sent yet
|
||||||
|
if (!headers_sent()) {
|
||||||
|
$isHttps = isHttps();
|
||||||
|
|
||||||
|
header("X-Frame-Options: SAMEORIGIN");
|
||||||
|
header("X-Content-Type-Options: nosniff");
|
||||||
|
header("X-XSS-Protection: 1; mode=block");
|
||||||
|
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||||
|
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||||
|
|
||||||
|
// HTTP_HOST is client-controllable, and it's about to be
|
||||||
|
// interpolated INTO an HTTP response header value (CSP). A
|
||||||
|
// CRLF in the Host header would split the response and let
|
||||||
|
// an attacker inject a second header. A semicolon, space,
|
||||||
|
// or quote could break out of the frame-src directive (e.g.
|
||||||
|
// closing it early to widen frame-ancestors). The original
|
||||||
|
// `preg_replace('/:\d+$/', '', ...)` only stripped the port
|
||||||
|
// — anything else in HTTP_HOST flowed straight into the CSP.
|
||||||
|
//
|
||||||
|
// Tighten the regex to keep only hostname-shaped characters.
|
||||||
|
// Same set as ssh_access.php's #19 fix: DNS names, IPv4,
|
||||||
|
// IPv6 bracketed, optional `:port`. Strip the port suffix
|
||||||
|
// afterwards (frame-src already says `:*` so a specific
|
||||||
|
// port would be redundant).
|
||||||
|
$rawHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
|
$hostnameOnly = preg_replace(
|
||||||
|
'/[^a-zA-Z0-9.\-\[\]]/',
|
||||||
|
'',
|
||||||
|
preg_replace('/:\d+$/', '', $rawHost)
|
||||||
|
);
|
||||||
|
if ($hostnameOnly === '') {
|
||||||
|
// No usable host (empty header, all-bad chars). Fall
|
||||||
|
// back to a safe constant so frame-src is well-formed
|
||||||
|
// — `localhost` will simply not match any real iframe
|
||||||
|
// target and the CSP rejects the embedding cleanly.
|
||||||
|
$hostnameOnly = 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CSP that allows frames from same hostname on any port
|
||||||
|
$imgSrc = $isHttps ? "'self' data: https:" : "'self' data: http: https:";
|
||||||
|
|
||||||
|
// Allow frames from same hostname with any port (for shellinabox, etc.)
|
||||||
|
$csp = "default-src 'self'; " .
|
||||||
|
"script-src 'self' 'unsafe-inline'; " .
|
||||||
|
"style-src 'self' 'unsafe-inline'; " .
|
||||||
|
"img-src {$imgSrc}; " .
|
||||||
|
"connect-src 'self'; " .
|
||||||
|
"frame-src 'self' http://{$hostnameOnly}:* https://{$hostnameOnly}:*; " .
|
||||||
|
"frame-ancestors 'self'";
|
||||||
|
|
||||||
|
header("Content-Security-Policy: " . $csp);
|
||||||
|
|
||||||
|
// Only add HSTS if served over HTTPS
|
||||||
|
if ($isHttps) {
|
||||||
|
header("Strict-Transport-Security: max-age=31536000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pi-Star Dashboard version string.
|
||||||
|
*
|
||||||
|
* Bumped on each release. Format: YYYYMMDD (calver).
|
||||||
|
* Surfaced in the dashboard banner and in the `meta generator` tag.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$version = '20260623';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
// Empty placeholder: presence prevents directory listing under Apache
|
||||||
|
// when no DirectoryIndex match is found in this folder.
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
.container {
|
||||||
|
width: 820px;
|
||||||
|
text-align: left;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, font {
|
||||||
|
font: 12px verdana,arial,sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background : #dd4b39;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #ffffff;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
text-align : left;
|
||||||
|
padding : 5px 0px 5px 0px;
|
||||||
|
border-radius : 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
float : left;
|
||||||
|
margin : 0;
|
||||||
|
padding : 3px 3px 3px 3px;
|
||||||
|
width : 160px;
|
||||||
|
background : #242d31;
|
||||||
|
font-weight : normal;
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin : 0 0 0 166px;
|
||||||
|
padding : 1px 5px 5px 5px;
|
||||||
|
color : #000000;
|
||||||
|
background : #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide {
|
||||||
|
padding: 5px 5px 5px 5px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background : #dd4b39;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #ffffff;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
font-size : 9px;
|
||||||
|
text-align : center;
|
||||||
|
padding : 10px 0 10px 0;
|
||||||
|
border-radius : 0 0 10px 10px;
|
||||||
|
clear : both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tail {
|
||||||
|
height: 450px;
|
||||||
|
width: 805px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: scroll;
|
||||||
|
color: #00ff00;
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
empty-cells: show;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-color: #000000;
|
||||||
|
border-style: solid;
|
||||||
|
border-spacing: 4px;
|
||||||
|
border-width: 2px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #000000;
|
||||||
|
font-family: verdana,arial,sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-shadow: 1px 1px #8b0000;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #dd4b39;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(even) {
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(odd) {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
color: #000000;
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #edf0f5;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link, a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0000e0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip, a.tooltip:link, a.tooltip:visited, a.tooltip:active {
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2, a.tooltip2:link, a.tooltip2:visited, a.tooltip2:active {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000000;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
list-style: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
float: left;
|
||||||
|
display: inline; /*For ignore double margin in IE6*/
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
float:left;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 900 14px/22px "Arial", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a span {
|
||||||
|
margin: 0 10px 0 -10px;
|
||||||
|
padding: 1px 8px 5px 18px;
|
||||||
|
position: relative; /*To fix IE6 problem (not displaying)*/
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current, ul.mmenu li a:hover {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top right;
|
||||||
|
color: #0d5f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current span, ul.mmenu li a:hover span {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 2px 2px #303030;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Toggle Code here */
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle + label {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label {
|
||||||
|
padding: 1px;
|
||||||
|
width: 33px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before,
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before {
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
right: 1px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: margin 0.4s, background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label:after {
|
||||||
|
margin-left: 14px;
|
||||||
|
background-color: #dd4b39;
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, font {
|
||||||
|
font: 12px verdana,arial,sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background : #dd4b39;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #ffffff;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
text-align : left;
|
||||||
|
padding : 5px 0px 5px 0px;
|
||||||
|
border-radius : 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: none;
|
||||||
|
float : left;
|
||||||
|
margin : 0;
|
||||||
|
padding : 3px 3px 3px 3px;
|
||||||
|
width : 160px;
|
||||||
|
background : #242d31;
|
||||||
|
font-weight : normal;
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 5px 5px 5px 5px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide {
|
||||||
|
padding: 5px 5px 5px 5px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background : #dd4b39;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #ffffff;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
font-size : 9px;
|
||||||
|
text-align : center;
|
||||||
|
padding : 10px 0 10px 0;
|
||||||
|
border-radius : 0 0 10px 10px;
|
||||||
|
clear : both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tail {
|
||||||
|
height: 450px;
|
||||||
|
width: 805px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: scroll;
|
||||||
|
color: #00ff00;
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
empty-cells: show;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-color: #000000;
|
||||||
|
border-style: solid;
|
||||||
|
border-spacing: 4px;
|
||||||
|
border-width: 2px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #000000;
|
||||||
|
font-family: verdana,arial,sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-shadow: 1px 1px #8b0000;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #dd4b39;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(even) {
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(odd) {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
color: #000000;
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #edf0f5;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link, a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0000e0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip, a.tooltip:link, a.tooltip:visited, a.tooltip:active {
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2, a.tooltip2:link, a.tooltip2:visited, a.tooltip2:active {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000000;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
list-style: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
float: left;
|
||||||
|
display: inline; /*For ignore double margin in IE6*/
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
float:left;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 900 14px/22px "Arial", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a span {
|
||||||
|
margin: 0 10px 0 -10px;
|
||||||
|
padding: 1px 8px 5px 18px;
|
||||||
|
position: relative; /*To fix IE6 problem (not displaying)*/
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current, ul.mmenu li a:hover {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top right;
|
||||||
|
color: #0d5f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current span, ul.mmenu li a:hover span {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 2px 2px #303030;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Toggle Code here */
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle + label {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label {
|
||||||
|
padding: 1px;
|
||||||
|
width: 33px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before,
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before {
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
right: 1px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: margin 0.4s, background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label:after {
|
||||||
|
margin-left: 14px;
|
||||||
|
background-color: #dd4b39;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.nice-select{-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:#fff;border-radius:5px;border:solid 1px #e8e8e8;box-sizing:border-box;clear:both;cursor:pointer;display:block;float:left;font-family:inherit;font-size:14px;font-weight:normal;height:38px;line-height:36px;outline:none;padding-left:18px;padding-right:30px;position:relative;text-align:left !important;transition:all .2s ease-in-out;user-select:none;white-space:nowrap;width:auto}.nice-select:hover{border-color:#dbdbdb}.nice-select:active,.nice-select.open,.nice-select:focus{border-color:#999}.nice-select:after{border-bottom:2px solid #999;border-right:2px solid #999;content:"";display:block;height:5px;margin-top:-4px;pointer-events:none;position:absolute;right:12px;top:50%;transform-origin:66% 66%;transform:rotate(45deg);transition:all .15s ease-in-out;width:5px}.nice-select.open:after{transform:rotate(-135deg)}.nice-select.open .nice-select-dropdown{opacity:1;pointer-events:auto;transform:scale(1) translateY(0)}.nice-select.disabled{border-color:#ededed;color:#999;pointer-events:none}.nice-select.disabled:after{border-color:#ccc}.nice-select.wide{width:100%}.nice-select.wide .nice-select-dropdown{left:0 !important;right:0 !important}.nice-select.right{float:right}.nice-select.right .nice-select-dropdown{left:auto;right:0}.nice-select.small{font-size:12px;height:36px;line-height:34px}.nice-select.small:after{height:4px;width:4px}.nice-select.small .option{line-height:34px;min-height:34px}.nice-select .nice-select-dropdown{margin-top:4px;background-color:#fff;border-radius:5px;box-shadow:0 0 0 1px rgba(68,68,68,.11);pointer-events:none;position:absolute;top:100%;left:0;transform-origin:50% 0;transform:scale(0.75) translateY(19px);transition:all .2s cubic-bezier(0.5, 0, 0, 1.25),opacity .15s ease-out;z-index:9;opacity:0}.nice-select .list{border-radius:5px;box-sizing:border-box;overflow:hidden;padding:0;max-height:210px;overflow-y:auto}.nice-select .list:hover .option:not(:hover){background-color:transparent !important}.nice-select .option{cursor:pointer;font-weight:400;line-height:40px;list-style:none;outline:none;padding-left:18px;padding-right:29px;text-align:left;transition:all .2s}.nice-select .option:hover,.nice-select .option.focus,.nice-select .option.selected.focus{background-color:#f6f6f6}.nice-select .option.selected{font-weight:bold}.nice-select .option.disabled{background-color:transparent;color:#999;cursor:default}.nice-select .optgroup{font-weight:bold}.no-csspointerevents .nice-select .nice-select-dropdown{display:none}.no-csspointerevents .nice-select.open .nice-select-dropdown{display:block}.nice-select .list::-webkit-scrollbar{width:0}.nice-select .has-multiple{white-space:inherit;height:auto;padding:7px 12px;min-height:36px;line-height:22px}.nice-select .has-multiple span.current{border:1px solid #ccc;background:#eee;padding:0 10px;border-radius:3px;display:inline-block;line-height:24px;font-size:14px;margin-bottom:3px;margin-right:3px}.nice-select .has-multiple .multiple-options{display:block;line-height:24px;padding:0}.nice-select .nice-select-search-box{box-sizing:border-box;width:100%;padding:5px;pointer-events:none;border-radius:5px 5px 0 0}.nice-select .nice-select-search{box-sizing:border-box;background-color:#fff;border:1px solid #e8e8e8;border-radius:3px;color:#444;display:inline-block;vertical-align:middle;padding:7px 12px;margin:0 10px 0 0;width:100%;min-height:36px;line-height:22px;height:auto;outline:0 !important;font-size:14px}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Themed dashboard stylesheet — narrow / mobile layout.
|
||||||
|
*
|
||||||
|
* Emits CSS to the browser with a `Content-type: text/css` header. Theme
|
||||||
|
* colours and table-row backgrounds are loaded from `/etc/pistar-css.ini`
|
||||||
|
* (an INI written by admin/expert/edit_dashboard.php) when that file
|
||||||
|
* exists, otherwise we fall back to the built-in palette below.
|
||||||
|
*
|
||||||
|
* Mobile counterpart of pistar-css.php — config/browserdetect.php picks
|
||||||
|
* which one to load based on the viewport / User-Agent. The PHP header
|
||||||
|
* (theme variable load) is identical across the two files; the CSS body
|
||||||
|
* differs in layout sizing.
|
||||||
|
*
|
||||||
|
* Do NOT edit this file by hand — operator-overridable colours live in
|
||||||
|
* /etc/pistar-css.ini via the expert dashboard editor. Hand edits will
|
||||||
|
* be overwritten on the next configuration save.
|
||||||
|
*
|
||||||
|
* Note: existing variable names like $bannerDropShaddows / $tableHeadDropShaddow
|
||||||
|
* preserve a long-standing typo ("Shaddow" should be "Shadow", "Headder"
|
||||||
|
* should be "Header"). Renaming them would break the CSS body below
|
||||||
|
* which references them by name; keep as-is for compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Output CSS, not the default text/html.
|
||||||
|
header('Content-type: text/css');
|
||||||
|
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
// Load the operator's theme overrides.
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile, 'r')) {
|
||||||
|
$piStarCss = parse_ini_file($piStarCssFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map parsed INI values onto the variables the CSS body interpolates.
|
||||||
|
$backgroundPage = $piStarCss['Background']['Page']; // page background (usually off-white)
|
||||||
|
$backgroundContent = $piStarCss['Background']['Content']; // white background in the content section
|
||||||
|
$backgroundBanners = $piStarCss['Background']['Banners']; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = $piStarCss['Text']['Banners']; // usually white
|
||||||
|
$bannerDropShaddows = $piStarCss['Text']['BannersDrop']; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = $piStarCss['Tables']['HeadDrop']; // table-header drop-shadow colour
|
||||||
|
$textContent = $piStarCss['Content']['Text']; // section title colour
|
||||||
|
$tableRowEvenBg = $piStarCss['Tables']['BgEven']; // table row background (even)
|
||||||
|
$tableRowOddBg = $piStarCss['Tables']['BgOdd']; // table row background (odd)
|
||||||
|
} else {
|
||||||
|
// Fallback palette used when /etc/pistar-css.ini is absent.
|
||||||
|
$backgroundPage = 'edf0f5'; // page background (usually off-white)
|
||||||
|
$backgroundContent = 'ffffff'; // white background in the content section
|
||||||
|
$backgroundBanners = 'dd4b39'; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = 'ffffff'; // usually white
|
||||||
|
$bannerDropShaddows = '303030'; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = '8b0000'; // table-header drop-shadow colour
|
||||||
|
$textContent = '000000'; // section title colour
|
||||||
|
$tableRowEvenBg = 'f7f7f7'; // table row background (even)
|
||||||
|
$tableRowOddBg = 'd0d0d0'; // table row background (odd)
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin: auto;
|
||||||
|
background : #<?php echo $backgroundContent; ?>;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
-moz-border-radius: 10px 10px 10px 10px;
|
||||||
|
-webkit-border-radius: 10px 10px 10px 10px;
|
||||||
|
-khtml-border-radius: 10px 10px 10px 10px;
|
||||||
|
-ms-border-radius: 10px 10px 10px 10px;
|
||||||
|
box-shadow: 3px 3px 3px #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, font {
|
||||||
|
font: 12px verdana,arial,sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background : #<?php echo $backgroundBanners; ?>;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #<?php echo $textBanners; ?>;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
text-align : left;
|
||||||
|
padding : 5px 0px 5px 0px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
-moz-border-radius: 10px 10px 0px 0px;
|
||||||
|
-webkit-border-radius: 10px 10px 0px 0px;
|
||||||
|
-khtml-border-radius: 10px 10px 0px 0px;
|
||||||
|
-ms-border-radius: 10px 10px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: none;
|
||||||
|
float : left;
|
||||||
|
margin : 0;
|
||||||
|
padding : 3px 3px 3px 3px;
|
||||||
|
width : 160px;
|
||||||
|
background : #242d31;
|
||||||
|
font-weight : normal;
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding : 5px 5px 5px 5px;
|
||||||
|
color : #<?php echo $textContent; ?>;
|
||||||
|
background : #<?php echo $backgroundContent; ?>;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide {
|
||||||
|
padding: 5px 5px 5px 5px;
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
background: #<?php echo $backgroundContent; ?>;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide h2 {
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
font: 1em verdana,arial,sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background : #<?php echo $backgroundBanners; ?>;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #<?php echo $textBanners; ?>;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
font-size : 9px;
|
||||||
|
text-align : center;
|
||||||
|
padding : 10px 0 10px 0;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
-moz-border-radius: 0px 0px 10px 10px;
|
||||||
|
-webkit-border-radius: 0px 0px 10px 10px;
|
||||||
|
-khtml-border-radius: 0px 0px 10px 10px;
|
||||||
|
-ms-border-radius: 0px 0px 10px 10px;
|
||||||
|
clear : both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tail {
|
||||||
|
height: 450px;
|
||||||
|
width: 805px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: scroll;
|
||||||
|
color: #00ff00;
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
empty-cells: show;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-color: #000000;
|
||||||
|
border-style: solid;
|
||||||
|
border-spacing: 4px;
|
||||||
|
border-width: 2px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #000000;
|
||||||
|
font-family: verdana,arial,sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-shadow: 1px 1px #<?php echo $tableHeadDropShaddow; ?>;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #<?php echo $backgroundBanners; ?>;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(even) {
|
||||||
|
background: #<?php echo $tableRowEvenBg; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(odd) {
|
||||||
|
background: #<?php echo $tableRowOddBg; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
color: #000000;
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #<?php echo $backgroundPage; ?>;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link, a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0000e0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip, a.tooltip:link, a.tooltip:visited, a.tooltip:active {
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2, a.tooltip2:link, a.tooltip2:visited, a.tooltip2:active {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000000;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
list-style: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
float: left;
|
||||||
|
display: inline; /*For ignore double margin in IE6*/
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
float:left;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 900 14px/22px "Arial", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a span {
|
||||||
|
margin: 0 10px 0 -10px;
|
||||||
|
padding: 1px 8px 5px 18px;
|
||||||
|
position: relative; /*To fix IE6 problem (not displaying)*/
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current, ul.mmenu li a:hover {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top right;
|
||||||
|
color: #0d5f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current span, ul.mmenu li a:hover span {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 2px 2px #<?php echo $bannerDropShaddows; ?>;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Toggle Code here */
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle + label {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label {
|
||||||
|
padding: 1px;
|
||||||
|
width: 33px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before,
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before {
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
right: 1px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: margin 0.4s, background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label:after {
|
||||||
|
margin-left: 14px;
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tame Firefox Buttons */
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
select,
|
||||||
|
input {
|
||||||
|
margin : 0;
|
||||||
|
padding : 0;
|
||||||
|
border-width : 1px;
|
||||||
|
font : 12px verdana,arial,sans-serif;
|
||||||
|
}
|
||||||
|
input[type="button"], button, input[type="submit"] {
|
||||||
|
padding : 0px 3px 0px 3px;
|
||||||
|
border-radius : 3px 3px 3px 3px;
|
||||||
|
-moz-border-radius : 3px 3px 3px 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* nice-select dropdowns */
|
||||||
|
.nice-select.small, .nice-select-dropdown li.option {
|
||||||
|
height: 24px !important;
|
||||||
|
min-height: 24px !important;
|
||||||
|
line-height: 24px !important;
|
||||||
|
}
|
||||||
|
.nice-select.small ul li:nth-of-type(2) {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Themed dashboard stylesheet — desktop / wide layout.
|
||||||
|
*
|
||||||
|
* Emits CSS to the browser with a `Content-type: text/css` header. Theme
|
||||||
|
* colours and table-row backgrounds are loaded from `/etc/pistar-css.ini`
|
||||||
|
* (an INI written by admin/expert/edit_dashboard.php) when that file
|
||||||
|
* exists, otherwise we fall back to the built-in palette below.
|
||||||
|
*
|
||||||
|
* Do NOT edit this file by hand — operator-overridable colours live in
|
||||||
|
* /etc/pistar-css.ini via the expert dashboard editor. Hand edits will
|
||||||
|
* be overwritten on the next configuration save.
|
||||||
|
*
|
||||||
|
* Note: existing variable names like $bannerDropShaddows / $tableHeadDropShaddow
|
||||||
|
* preserve a long-standing typo ("Shaddow" should be "Shadow", "Headder"
|
||||||
|
* should be "Header"). Renaming them would break the CSS body below
|
||||||
|
* which references them by name; keep as-is for compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Output CSS, not the default text/html.
|
||||||
|
header('Content-type: text/css');
|
||||||
|
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
// Load the operator's theme overrides.
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile, 'r')) {
|
||||||
|
$piStarCss = parse_ini_file($piStarCssFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map parsed INI values onto the variables the CSS body interpolates.
|
||||||
|
$backgroundPage = $piStarCss['Background']['Page']; // page background (usually off-white)
|
||||||
|
$backgroundContent = $piStarCss['Background']['Content']; // white background in the content section
|
||||||
|
$backgroundBanners = $piStarCss['Background']['Banners']; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = $piStarCss['Text']['Banners']; // usually white
|
||||||
|
$bannerDropShaddows = $piStarCss['Text']['BannersDrop']; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = $piStarCss['Tables']['HeadDrop']; // table-header drop-shadow colour
|
||||||
|
$textContent = $piStarCss['Content']['Text']; // section title colour
|
||||||
|
$tableRowEvenBg = $piStarCss['Tables']['BgEven']; // table row background (even)
|
||||||
|
$tableRowOddBg = $piStarCss['Tables']['BgOdd']; // table row background (odd)
|
||||||
|
} else {
|
||||||
|
// Fallback palette used when /etc/pistar-css.ini is absent.
|
||||||
|
$backgroundPage = 'edf0f5'; // page background (usually off-white)
|
||||||
|
$backgroundContent = 'ffffff'; // white background in the content section
|
||||||
|
$backgroundBanners = 'dd4b39'; // the ubiquitous Pi-Star red
|
||||||
|
$textBanners = 'ffffff'; // usually white
|
||||||
|
$bannerDropShaddows = '303030'; // banner drop-shadow colour
|
||||||
|
$tableHeadDropShaddow = '8b0000'; // table-header drop-shadow colour
|
||||||
|
$textContent = '000000'; // section title colour
|
||||||
|
$tableRowEvenBg = 'f7f7f7'; // table row background (even)
|
||||||
|
$tableRowOddBg = 'd0d0d0'; // table row background (odd)
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
.container {
|
||||||
|
width: 820px;
|
||||||
|
text-align: left;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
-moz-border-radius: 10px 10px 10px 10px;
|
||||||
|
-webkit-border-radius: 10px 10px 10px 10px;
|
||||||
|
-khtml-border-radius: 10px 10px 10px 10px;
|
||||||
|
-ms-border-radius: 10px 10px 10px 10px;
|
||||||
|
box-shadow: 3px 3px 3px #707070;
|
||||||
|
background : #<?php echo $backgroundContent; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, font {
|
||||||
|
font: 12px verdana,arial,sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background : #<?php echo $backgroundBanners; ?>;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #<?php echo $textBanners; ?>;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
text-align : left;
|
||||||
|
padding : 5px 0px 5px 0px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
-moz-border-radius: 10px 10px 0px 0px;
|
||||||
|
-webkit-border-radius: 10px 10px 0px 0px;
|
||||||
|
-khtml-border-radius: 10px 10px 0px 0px;
|
||||||
|
-ms-border-radius: 10px 10px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
float : left;
|
||||||
|
margin : 0;
|
||||||
|
padding : 3px 3px 3px 3px;
|
||||||
|
width : 160px;
|
||||||
|
background : #242d31;
|
||||||
|
font-weight : normal;
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin : 0 0 0 166px;
|
||||||
|
padding : 1px 5px 5px 5px;
|
||||||
|
color : #<?php echo $textContent; ?>;
|
||||||
|
background : #<?php echo $backgroundContent; ?>;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide {
|
||||||
|
padding: 5px 5px 5px 5px;
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
background: #<?php echo $backgroundContent; ?>;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentwide h2 {
|
||||||
|
color: #<?php echo $textContent; ?>;
|
||||||
|
font: 1em verdana,arial,sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background : #<?php echo $backgroundBanners; ?>;
|
||||||
|
text-decoration : none;
|
||||||
|
color : #<?php echo $textBanners; ?>;
|
||||||
|
font-family : verdana, arial, sans-serif;
|
||||||
|
font-size : 9px;
|
||||||
|
text-align : center;
|
||||||
|
padding : 10px 0 10px 0;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
-moz-border-radius: 0px 0px 10px 10px;
|
||||||
|
-webkit-border-radius: 0px 0px 10px 10px;
|
||||||
|
-khtml-border-radius: 0px 0px 10px 10px;
|
||||||
|
-ms-border-radius: 0px 0px 10px 10px;
|
||||||
|
clear : both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tail {
|
||||||
|
height: 450px;
|
||||||
|
width: 805px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: scroll;
|
||||||
|
color: #00ff00;
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
empty-cells: show;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-color: #000000;
|
||||||
|
border-style: solid;
|
||||||
|
border-spacing: 4px;
|
||||||
|
border-width: 2px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #000000;
|
||||||
|
font-family: verdana,arial,sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-shadow: 1px 1px #<?php echo $tableHeadDropShaddow; ?>;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #<?php echo $backgroundBanners; ?>;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(even) {
|
||||||
|
background: #<?php echo $tableRowEvenBg; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(odd) {
|
||||||
|
background: #<?php echo $tableRowOddBg; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
color: #000000;
|
||||||
|
font-family: "Lucidia Console",Monaco,monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #<?php echo $backgroundPage; ?>;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link, a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0000e0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip, a.tooltip:link, a.tooltip:visited, a.tooltip:active {
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child a.tooltip:hover span {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2, a.tooltip2:link, a.tooltip2:visited, a.tooltip2:active {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000000;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2:hover span {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 202px;
|
||||||
|
z-index: 100;
|
||||||
|
color: #000000;
|
||||||
|
border:1px solid #000000;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font: 12px Verdana, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tooltip2 span b {
|
||||||
|
text-shadow: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
list-style: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
float: left;
|
||||||
|
display: inline; /*For ignore double margin in IE6*/
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
float:left;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 900 14px/22px "Arial", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li a span {
|
||||||
|
margin: 0 10px 0 -10px;
|
||||||
|
padding: 1px 8px 5px 18px;
|
||||||
|
position: relative; /*To fix IE6 problem (not displaying)*/
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current, ul.mmenu li a:hover {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top right;
|
||||||
|
color: #0d5f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.mmenu li a.current span, ul.mmenu li a:hover span {
|
||||||
|
background: url(/images/buttonbg.png) no-repeat top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 2px 2px #<?php echo $bannerDropShaddows; ?>;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Toggle Code here */
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle + label {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label {
|
||||||
|
padding: 1px;
|
||||||
|
width: 33px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before,
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:before {
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
right: 1px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat + label:after {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: margin 0.4s, background 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label {
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.toggle-round-flat:checked + label:after {
|
||||||
|
margin-left: 14px;
|
||||||
|
background-color: #<?php echo $backgroundBanners; ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tame Firefox Buttons */
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
select,
|
||||||
|
input {
|
||||||
|
margin : 0;
|
||||||
|
padding : 0;
|
||||||
|
border-width : 1px;
|
||||||
|
font : 12px verdana,arial,sans-serif;
|
||||||
|
}
|
||||||
|
input[type="button"], button, input[type="submit"] {
|
||||||
|
padding : 0px 3px 0px 3px;
|
||||||
|
border-radius : 3px 3px 3px 3px;
|
||||||
|
-moz-border-radius : 3px 3px 3px 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* nice-select dropdowns */
|
||||||
|
.nice-select.small, .nice-select-dropdown li.option {
|
||||||
|
height: 24px !important;
|
||||||
|
min-height: 24px !important;
|
||||||
|
line-height: 24px !important;
|
||||||
|
}
|
||||||
|
.nice-select.small ul li:nth-of-type(2) {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aria CSS Here
|
||||||
|
[role="checkbox"] {
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-checked="true"]::before {
|
||||||
|
content: "[x]";
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-checked="false"]::before {
|
||||||
|
content: "[ ]";
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* D-Star reflector link status — one row per repeater module (A-D).
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial; refreshed every 15 seconds by /index.php. Used
|
||||||
|
* in BOTH MMDVMHost mode (when D-Star Network is enabled) and
|
||||||
|
* dstarrepeater mode.
|
||||||
|
*
|
||||||
|
* For each `repeaterBand1`..`4` configured in /etc/ircddbgateway,
|
||||||
|
* inspects /var/log/pi-star/Links.log for the most recent link state
|
||||||
|
* line (DExtra / DPlus / DCS / CCS) targeting that module's callsign.
|
||||||
|
* Outputs columns: callsign, linked-to reflector, protocol, direction
|
||||||
|
* (Incoming / Outgoing), last-change timestamp.
|
||||||
|
*
|
||||||
|
* The log line shapes this file matches are documented inline as
|
||||||
|
* comments next to each preg_match_all — preserve those samples
|
||||||
|
* verbatim if Links.log format ever drifts.
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial — embeddable variant only. Omits the
|
||||||
|
* X-Frame-Options / frame-ancestors directives that the parent
|
||||||
|
* /index.php already asserts; they apply to iframe ancestry, not
|
||||||
|
* to XHR responses, so emitting them here is just dead bytes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setEmbeddableSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
?>
|
||||||
|
<b><?php echo $lang['d-star_link_status'];?></b>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><a class="tooltip" href="#">Radio<span><b>Radio Module</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Default<span><b>Default Link Destination</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Auto<span><b>AutoLink</b>- green: enabled<br />- red: disabled</span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Timer<span><b>Reset/Restart Timer</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Link<span><b>Link-Status</b>- green: enabled<br />- red: disabled</span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Linked to<span><b>Linked Destination</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Mode<span><b>Mode or Protocol used</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Direction<span><b>Direction</b>Incoming or Outgoing</span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Last Change (<?php echo date('T')?>)<span><b>Timestamp of last change</b>Time of last change in <?php echo date('T')?> time zone</span></a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$tot = array(0=>"Never",1=>"Fixed",2=>"5min",3=>"10min",4=>"15min",5=>"20min",6=>"25min",7=>"30min",8=>"60min",9=>"90min",10=>"120min",11=>"180min",12=>" ");
|
||||||
|
$ci = 0;
|
||||||
|
$tr = 0;
|
||||||
|
for($i = 1;$i < 5; $i++){
|
||||||
|
$param="repeaterBand" . $i;
|
||||||
|
if((isset($configs[$param])) && strlen($configs[$param]) == 1) {
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
print "<tr>";
|
||||||
|
$tr = 1;
|
||||||
|
$module = $configs[$param];
|
||||||
|
$rcall = sprintf("%-7.7s%-1.1s",$MYCALL,$module);
|
||||||
|
$param="repeaterCall" . $i;
|
||||||
|
if(isset($configs[$param])) { $rptrcall=sprintf("%-7.7s%-1.1s",$configs[$param],$module); } else { $rptrcall = $rcall;}
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($rptrcall,0,8))."</td>";
|
||||||
|
$param="reflector" . $i;
|
||||||
|
if(isset($configs[$param])) { print "<td>".str_replace(' ', ' ', substr($configs[$param],0,8))."</td>"; } else { print "<td> </td>";}
|
||||||
|
$param="atStartup" . $i;
|
||||||
|
if($configs[$param] == 1){print "<td>Auto</td>"; } else { print "<td>No</td>"; }
|
||||||
|
$param="reconnect" . $i;
|
||||||
|
if(isset($configs[$param])) { $t = $configs[$param]; } else { $t = 0; }
|
||||||
|
if($t > 12){ $t = 12; }
|
||||||
|
print "<td>$tot[$t]</td>";
|
||||||
|
$j=0;
|
||||||
|
if ($linkLog = @fopen($linkLogPath,'r')) {
|
||||||
|
while ($linkLine = fgets($linkLog)) {
|
||||||
|
//$statimg = "<img src=\"images/20red.png\">";
|
||||||
|
$statimg = "Down";
|
||||||
|
$linkDate = " ";
|
||||||
|
$protocol = " ";
|
||||||
|
$linkType = " ";
|
||||||
|
$linkRptr = " ";
|
||||||
|
$linkRefl = " ";
|
||||||
|
// Reflector-Link, sample:
|
||||||
|
// 2011-09-22 02:15:06: DExtra link - Type: Repeater Rptr: DB0LJ B Refl: XRF023 A Dir: Outgoing
|
||||||
|
// 2012-10-12 17:15:45: DCS link - Type: Repeater Rptr: DB0LJ B Refl: DCS001 L Dir: Outgoing
|
||||||
|
// 2012-10-12 17:56:10: DCS link - Type: Repeater Rptr: DB0RPL B Refl: DCS015 B Dir: Outgoing
|
||||||
|
if(preg_match_all('/^(.{19}).*(D[A-Za-z]*).*Type: ([A-Za-z]*).*Rptr: (.{8}).*Refl: (.{8}).*Dir: Outgoing$/',$linkLine,$linx) > 0){
|
||||||
|
$statimg = "Up";
|
||||||
|
$linkDate = date("d-M-Y H:i:s", strtotime(substr($linx[1][0],0,19)));
|
||||||
|
$protocol = $linx[2][0];
|
||||||
|
$linkType = $linx[3][0];
|
||||||
|
$linkRptr = $linx[4][0];
|
||||||
|
$linkRefl = $linx[5][0];
|
||||||
|
if($linkRptr == $rptrcall){
|
||||||
|
print "<td>$statimg</td>";
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($linkRefl,0,8))."</td>";
|
||||||
|
print "<td>$protocol</td>";
|
||||||
|
print "<td>Outgoing</td>";
|
||||||
|
$utc_time = $linkDate;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
print "<td>$local_time</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
$tr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($linkLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tr == 1){
|
||||||
|
print"<td>Down</td><td>None</td><td>--</td><td>----</td><td>----</td></tr>\n";
|
||||||
|
}
|
||||||
|
// 00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122
|
||||||
|
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
|
||||||
|
// 2012-05-08 21:16:31: DExtra link - Type: Repeater Rptr: DB0LJ A Refl: DB0MYK B Dir: Incoming
|
||||||
|
// 2012-05-08 21:16:31: DPlus link - Type: Dongle User: W1CDG H Dir: Incoming
|
||||||
|
if ($linkLog = @fopen($linkLogPath,'r')) {
|
||||||
|
while ($linkLine = fgets($linkLog)) {
|
||||||
|
$statimg = "Down";
|
||||||
|
$linkDate = " ";
|
||||||
|
$protocol = " ";
|
||||||
|
$linkType = " ";
|
||||||
|
$linkRptr = " ";
|
||||||
|
$linkRefl = " ";
|
||||||
|
if(preg_match_all('/^(.{19}).*(D[A-Za-z]*).*Type: ([A-Za-z]*).*Rptr: (.{8}).*Refl: (.{8}).*Dir: Incoming$/',$linkLine,$linx) > 0){
|
||||||
|
$statimg = "Up";
|
||||||
|
$linkDate = date("d-M-Y H:i:s", strtotime(substr($linx[1][0],0,19)));
|
||||||
|
$protocol = $linx[2][0];
|
||||||
|
$linkType = $linx[3][0];
|
||||||
|
$linkRptr = $linx[4][0];
|
||||||
|
$linkRefl = $linx[5][0];
|
||||||
|
if($linkRptr == $rptrcall){
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
print "<tr>";
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($rptrcall,0,8))."</td>";
|
||||||
|
print "<td> </td><td> </td><td> </td>";
|
||||||
|
print "<td>$statimg</td>";
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($linkRefl,0,8))."</td>";
|
||||||
|
print "<td>$protocol</td>";
|
||||||
|
print "<td>Incoming</td>";
|
||||||
|
$utc_time = $linkDate;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
print "<td>$local_time</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
//$tr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($linkLog);
|
||||||
|
}
|
||||||
|
// 00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122
|
||||||
|
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
|
||||||
|
// 2012-05-08 21:16:31: DExtra link - Type: Repeater Rptr: DB0LJ A Refl: DB0MYK B Dir: Incoming
|
||||||
|
// 2012-05-08 21:16:31: DPlus link - Type: Dongle User: W1CDG H Dir: Incoming
|
||||||
|
if ($linkLog = @fopen($linkLogPath,'r')) {
|
||||||
|
while ($linkLine = fgets($linkLog)) {
|
||||||
|
$statimg = "Down";
|
||||||
|
$linkDate = " ";
|
||||||
|
$protocol = " ";
|
||||||
|
$linkType = " ";
|
||||||
|
$linkRptr = " ";
|
||||||
|
$linkRefl = " ";
|
||||||
|
if(preg_match_all('/^(.{19}).*(D[A-Za-z]*).*Type: ([A-Za-z]*).*User: (.[^\s]+).*Dir: Incoming$/',$linkLine,$linx) > 0){
|
||||||
|
$statimg = "Up";
|
||||||
|
$linkDate = date("d-M-Y H:i:s", strtotime(substr($linx[1][0],0,19)));
|
||||||
|
$protocol = $linx[2][0];
|
||||||
|
$linkType = $linx[3][0];
|
||||||
|
$linkRptr = $linx[4][0];
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
print "<tr>";
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($rptrcall,0,8))."</td>";
|
||||||
|
print "<td> </td><td> </td><td> </td>";
|
||||||
|
print "<td>$statimg</td>";
|
||||||
|
print "<td>".str_replace(' ', ' ', substr($linkRptr,0,8))."</td>";
|
||||||
|
print "<td>$protocol</td>";
|
||||||
|
print "<td>Incoming</td>";
|
||||||
|
$utc_time = $linkDate;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
print "<td>$local_time</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($linkLog);
|
||||||
|
}
|
||||||
|
// End
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* StarNet groups + active members partial.
|
||||||
|
*
|
||||||
|
* Sub-partial included by css_connections.php when at least one
|
||||||
|
* `starNetCallsign1`..`5` is configured in /etc/ircddbgateway.
|
||||||
|
*
|
||||||
|
* Two tables:
|
||||||
|
* 1. Configured groups — callsign, logoff callsign, info text,
|
||||||
|
* user/group timeouts. Pulled directly from /etc/ircddbgateway.
|
||||||
|
* 2. Active members — parsed from /var/log/pi-star/STARnet.log via
|
||||||
|
* a preg_match_all that walks every "Adding/Removing <call> from
|
||||||
|
* StarNet group <group>" line. We treat the log as the
|
||||||
|
* authoritative state: an "Adding" entry inserts; a "Removing"
|
||||||
|
* entry unsets. The final $groupsx map then holds current members.
|
||||||
|
*
|
||||||
|
* No security_headers call — it's a sub-sub-partial. Flagged for the
|
||||||
|
* security pass.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
?>
|
||||||
|
<b><?php echo $lang['active_starnet_groups'];?></b>
|
||||||
|
<table style="table-layout: fixed;">
|
||||||
|
<tr>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['callsign'];?><span><b>Starnet Callsign</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['logoff'];?><span><b>Starnet Logoff Callsign</b></span></a></th>
|
||||||
|
<th colspan="3"><a class="tooltip" href="#"><?php echo $lang['info'];?><span><b>Infotext</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['utot'];?><span><b>User TimeOut (min)</b>inactivity time after which a user will be disconnected</span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['gtot'];?><span><b>Group TimeOut (min)</b>inactivity time after which the group will be disconnected</span></a></th>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
$ci = 0;
|
||||||
|
$i = 0;
|
||||||
|
$stngrp = array();
|
||||||
|
for($i = 1;$i < 6; $i++){
|
||||||
|
$param="starNetCallsign" . $i;
|
||||||
|
if(isset($configs[$param])) {
|
||||||
|
$gname = $configs[$param];
|
||||||
|
$stngrp[$gname] = $i;
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
print "<tr>";
|
||||||
|
print "<td align=\"center\">".str_replace(' ', ' ', substr($gname,0,8))."</td>";
|
||||||
|
$param="starNetLogoff" . $i;
|
||||||
|
if(isset($configs[$param])){ $output = str_replace(' ', ' ', substr($configs[$param],0,8)); print "<td align=\"center\">$output</td>";} else { print"<td> </td>";}
|
||||||
|
$param="starNetInfo" . $i;
|
||||||
|
if(isset($configs[$param])){ print "<td colspan=\"3\" align=\"left\">$configs[$param]</td>";} else { print"<td colspan=\"3\"> </td>";}
|
||||||
|
$param="starNetUserTimeout" . $i;
|
||||||
|
if(isset($configs[$param])){ print "<td align=\"center\">$configs[$param]</td>";} else { print"<td> </td>";}
|
||||||
|
$param="starNetGroupTimeout" . $i;
|
||||||
|
if(isset($configs[$param])){ print "<td align=\"center\">$configs[$param]</td>";} else { print"<td> </td>";}
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</table><br />
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$groupsx = array();
|
||||||
|
if ($starLog = fopen($starLogPath,'r')) {
|
||||||
|
while($logLine = fgets($starLog)) {
|
||||||
|
preg_match_all('/^(.{19}).*(Adding|Removing) (.{8}).*StarNet group (.{8}).*$/',$logLine,$matches);
|
||||||
|
$groupz = substr($matches[4][0],0,8);
|
||||||
|
$member = substr($matches[3][0],0,8);
|
||||||
|
$action = substr($matches[2][0],0,8);
|
||||||
|
$date = $matches[1][0];
|
||||||
|
$guid = $stngrp[$groupz];
|
||||||
|
if ($action == 'Adding'){
|
||||||
|
$groupsx[$guid][$groupz][$member] = $date;
|
||||||
|
}
|
||||||
|
elseif ($action == 'Removing'){
|
||||||
|
unset($groupsx[$guid][$groupz][$member]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($starLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clean the empty arrays from the multidimensional array
|
||||||
|
$groupsx = array_map('array_filter', $groupsx);
|
||||||
|
|
||||||
|
$active = 0;
|
||||||
|
for ($i = 1;$i < 6; $i++) {
|
||||||
|
if (isset($groupsx[$i])) {
|
||||||
|
$active = $active + count($groupsx[$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($active >= 1) {
|
||||||
|
|
||||||
|
echo "<b>".$lang['active_starnet_members']."</b>\n";
|
||||||
|
echo "<table style=\"table-layout: fixed;\">\n";
|
||||||
|
echo "<tr>\n";
|
||||||
|
echo "<th><a class=tooltip href=\"#\">".$lang['time']." (".date('T').")<span><b>Time of Login</b></span></a></th>\n";
|
||||||
|
echo "<th><a class=tooltip href=\"#\">".$lang['group']."<span><b>Starnet Callsign</b></span></a></th>\n";
|
||||||
|
echo "<th><a class=tooltip href=\"#\">".$lang['callsign']."<span><b>Callsign</b></span></a></th>\n";
|
||||||
|
echo "</tr>\n";
|
||||||
|
|
||||||
|
$ci = 0;
|
||||||
|
$ulist = array();
|
||||||
|
$glist = array();
|
||||||
|
for($i = 1;$i < 6; $i++){
|
||||||
|
if(isset($groupsx[$i])){
|
||||||
|
$glist = $groupsx[$i];
|
||||||
|
foreach ($glist as $gcall => $ulist){
|
||||||
|
foreach ($ulist as $ucall => $ulogin){
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
$ulogin = date("d-M-Y H:i:s", strtotime(substr($ulogin,0,19)));
|
||||||
|
$utc_time = $ulogin;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
$groupz = str_replace(' ', ' ', substr($gcall,0,8));
|
||||||
|
$ucall = str_replace(' ', '', substr($ucall,0,8));
|
||||||
|
print "<tr>";
|
||||||
|
print "<td align=\"left\">$local_time</td>";
|
||||||
|
print "<td align=\"center\">$groupz</td>";
|
||||||
|
print "<td align=\"center\"><a href=\"http://www.qrz.com/db/$ucall\" target=\"_new\" alt=\"Lookup Callsign\">$ucall</a></td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</table>\n<br />\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CCS (Connected Community System) connections + StarNet groups partial.
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial; refreshed every 15 seconds by /index.php. Two
|
||||||
|
* sections rendered conditionally:
|
||||||
|
* 1. Active CCS Connections — read from /var/log/pi-star/Links.log,
|
||||||
|
* surfaced only if the log contains at least one "CCS link" line.
|
||||||
|
* 2. StarNet groups — included via active_starnet_groups.php only when
|
||||||
|
* `starNetCallsign1`..`5` are configured in /etc/ircddbgateway.
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial — embeddable variant only. Omits the
|
||||||
|
* X-Frame-Options / frame-ancestors directives the parent already
|
||||||
|
* asserts; they apply to iframe ancestry, not XHR responses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setEmbeddableSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
?>
|
||||||
|
<?php
|
||||||
|
if (exec('grep "CCS link" '.$linkLogPath.' | wc -l') >=1) {
|
||||||
|
?>
|
||||||
|
<b>Active CCS Connections</b>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><a class=tooltip href="#">Repeater<span><b>Callsign of connected repeater</b></span></a></th>
|
||||||
|
<th><a class=tooltip href="#">Linked to<span><b>Actual link status</b></span></a></th>
|
||||||
|
<th><a class=tooltip href="#">Protocol<span><b>Protocol</b></span></a></th>
|
||||||
|
<th><a class=tooltip href="#">Direction<span><b>Direction</b>incoming or outgoing</span></a></th>
|
||||||
|
<th><a class=tooltip href="#">Last Change (<?php echo date('T')?>)<span><b>Timestamp of last change</b><?php echo date('T')?></span></a></th>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$ci = 0;
|
||||||
|
if ($linkLog = fopen($linkLogPath,'r')) {
|
||||||
|
$i=0;
|
||||||
|
while ($linkLine = fgets($linkLog)) {
|
||||||
|
// 2013-02-27 19:49:27: CCS link - Rptr: DB0LJ B Remote: DL5DI Dir: Incoming
|
||||||
|
if(preg_match_all('/^(.{19}).*(C[A-Za-z]*).*Rptr: (.{8}).*Remote: (.{8}).*Dir: (.{8})$/',$linkLine,$linx) > 0){
|
||||||
|
$utc_time = $linx[1][0];
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
$linkDate = $local_time;
|
||||||
|
$linkType = $linx[2][0];
|
||||||
|
$linkRptr = $linx[3][0];
|
||||||
|
$linkRem = $linx[4][0];
|
||||||
|
$linkDir = $linx[5][0];
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
print "<tr>";
|
||||||
|
print "<td>$linkRptr</td>";
|
||||||
|
print "<td>$linkRem</td>";
|
||||||
|
print "<td>CCS</td>";
|
||||||
|
print "<td>$linkDir</td>";
|
||||||
|
print "<td>$linkDate</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($linkLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
print "</table>\n<br />\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$stn_is_set = 0;
|
||||||
|
for($i = 1;$i < 6; $i++){
|
||||||
|
$param="starNetCallsign" . $i;
|
||||||
|
if(isset($configs[$param])) {
|
||||||
|
$stn_is_set = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($stn_is_set > 0){
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/dstarrepeater/active_starnet_groups.php';
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<table>
|
||||||
|
<tr><th>ircDDB Network</th><th>APRS Host</th><th>CCS</th><th>DCS</th><th>DExtra</th><th>DPlus</th><th>D-Rats</th><th>Info</th><th>ircDDB</th><th>Echo</th><th>Log</th></tr>
|
||||||
|
<tr style="background: #ffffff;">
|
||||||
|
<td><?php print $configs['ircddbHostname']; ?></td>
|
||||||
|
<td><?php if($configs['aprsEnabled'] == 1){ print $configs['aprsHostname']; } else { print "<img src=\"images/20red.png\">";} ?></td>
|
||||||
|
<?php
|
||||||
|
if($configs['ccsEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['dcsEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['dextraEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['dplusEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['dratsEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['infoEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['ircddbEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['echoEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
if($configs['logEnabled'] == 1){print "<td style=\"background-color:#1d1;\">ON</td>"; } else { print "<td style=\"background:#606060; color:#b0b0b0;\">OFF</td>"; }
|
||||||
|
?>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Last-heard list (D-Star repeater mode) — last 20 D-Star transmissions.
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial; refreshed every ~3 seconds by /index.php in
|
||||||
|
* dstarrepeater mode. Renders a 5-column table: time, callsign, target,
|
||||||
|
* Rpt 1, Rpt 2. Each callsign links to the operator's chosen lookup
|
||||||
|
* service (RadioID or QRZ, sourced from /etc/pistar-css.ini's
|
||||||
|
* [Lookup] Service key) and to aprs.fi for dPRS data.
|
||||||
|
*
|
||||||
|
* Data flow: shells out to a `grep|sort|head` pipeline against
|
||||||
|
* `/var/log/pi-star/Headers.log` to build /tmp/lastheard.log, then
|
||||||
|
* regex-parses each line. The "Headers.log sample" comment block
|
||||||
|
* below documents the exact line shape the regex targets — preserve
|
||||||
|
* those examples verbatim if log format ever drifts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
// AJAX-loaded partial — embeddable variant only. See the note in
|
||||||
|
// mmdvmhost/lh.php for why the historical double-call was wrong.
|
||||||
|
setEmbeddableSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
|
||||||
|
// Check if the config file exists
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
// Use the values from the file
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile,'r')) { $piStarCss = parse_ini_file($piStarCssFile, true); }
|
||||||
|
|
||||||
|
// Set the Values from the config file
|
||||||
|
if (isset($piStarCss['Lookup']['Service'])) { $callsignLookupSvc = $piStarCss['Lookup']['Service']; } // Lookup Service "QRZ" or "RadioID"
|
||||||
|
else { $callsignLookupSvc = "RadioID"; } // Set the default if its missing // Set the default if its missing
|
||||||
|
} else {
|
||||||
|
// Default values
|
||||||
|
$callsignLookupSvc = "RadioID";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety net
|
||||||
|
if (($callsignLookupSvc != "RadioID") && ($callsignLookupSvc != "QRZ")) { $callsignLookupSvc = "RadioID"; }
|
||||||
|
|
||||||
|
// Setup the URL(s)
|
||||||
|
if ($callsignLookupSvc == "RadioID") { $callsignLookupUrl = "https://database.radioid.net/database/view?callsign="; }
|
||||||
|
if ($callsignLookupSvc == "QRZ") { $callsignLookupUrl = "https://www.qrz.com/db/"; }
|
||||||
|
|
||||||
|
?>
|
||||||
|
<b><?php echo $lang['last_heard_list'];?></b>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['time'];?> (<?php echo date('T')?>)</a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['callsign'];?></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['target'];?></a></th>
|
||||||
|
<th><a class="tooltip" href="#">RPT 1</a></th>
|
||||||
|
<th><a class="tooltip" href="#">RPT 2</a></th>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
// Headers.log sample:
|
||||||
|
// 0000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122
|
||||||
|
// 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
|
||||||
|
// 2012-06-05 12:18:41: DCS header - My: PU2ZHZ /T Your: CQCQCQ Rpt1: PU2ZHZ B Rpt2: DCS007 B Flags: 00 00 00
|
||||||
|
// 2012-05-29 21:33:56: DPlus header - My: PD1RB /IC92 Your: CQCQCQ Rpt1: PE1RJV B Rpt2: REF017 A Flags: 00 00 00
|
||||||
|
// 2013-02-09 13:49:57: DExtra header - My: DO7MT / Your: CQCQCQ Rpt1: XRF001 G Rpt2: XRF001 C Flags: 00 00 00
|
||||||
|
//
|
||||||
|
exec('(grep -v " /TIME" '.$hdrLogPath.'|sort -r -k7,7|sort -u -k7,8|sort -r|head -20 >/tmp/lastheard.log) 2>&1 &');
|
||||||
|
$ci = 0;
|
||||||
|
if ($LastHeardLog = fopen("/tmp/lastheard.log",'r')) {
|
||||||
|
while ($linkLine = fgets($LastHeardLog)) {
|
||||||
|
if(preg_match_all('/^(.{19}).*My: (.*).*Your: (.*).*Rpt1: (.*).*Rpt2: (.*).*Flags: (.*)$/',$linkLine,$linx) > 0){
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
$QSODate = date("d-M-Y H:i:s", strtotime(substr($linx[1][0],0,19)));
|
||||||
|
|
||||||
|
// Every value below is parsed out of /var/log/pi-star/Headers.log
|
||||||
|
// — log lines produced by RF traffic, where the
|
||||||
|
// transmitting station sets the callsign field. Without
|
||||||
|
// escaping, a hostile callsign lands in the dashboard's
|
||||||
|
// ~3 second AJAX refresh. Normalise once here so each
|
||||||
|
// echo below works on safe values.
|
||||||
|
//
|
||||||
|
// $myCallRaw — raw 8-char callsign window
|
||||||
|
// $myCallHtml — HTML-safe display form
|
||||||
|
// $myCallLink — first whitespace-delimited token (the
|
||||||
|
// callsign without the space-padded
|
||||||
|
// suffix) — used for href URLs
|
||||||
|
// $myCallLinkHtml / $myCallLinkUrl — the URL forms
|
||||||
|
// $myIdHtml — 4-char ID after the `/` (HTML-safe)
|
||||||
|
// $yourCallHtml / $rpt1Html / $rpt2Html — target +
|
||||||
|
// repeaters with ` ` substitution applied AFTER
|
||||||
|
// escaping so the literal entity isn't double-encoded.
|
||||||
|
$myCallRaw = str_replace(' ', '', substr($linx[2][0],0,8));
|
||||||
|
$myCallHtml = htmlspecialchars($myCallRaw, ENT_QUOTES, 'UTF-8');
|
||||||
|
$myCallLink = strtok(substr($linx[2][0],0,8), " ");
|
||||||
|
$myCallLinkUrl = rawurlencode((string)$myCallLink);
|
||||||
|
$myIdHtml = htmlspecialchars(str_replace(' ', '', substr($linx[2][0],9,4)), ENT_QUOTES, 'UTF-8');
|
||||||
|
$yourCallHtml = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[3][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
$rpt1Html = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[4][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
$rpt2Html = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[5][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
|
||||||
|
$utc_time = $QSODate;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
print "<tr>";
|
||||||
|
print "<td align=\"left\">$local_time</td>";
|
||||||
|
print "<td align=\"left\" width=\"180\"><div style=\"float:left;\"><a href=\"".$callsignLookupUrl.$myCallLinkUrl."\" target=\"_blank\">$myCallHtml</a>";
|
||||||
|
if($myIdHtml !== '') { print "/$myIdHtml</div> <div style=\"text-align:right;\">(<a href=\"https://aprs.fi/#!call=".$myCallLinkUrl."*\" target=\"_blank\">dPRS</a>)</div></td>"; }
|
||||||
|
else { print "</div> <div style=\"text-align:right;\">(<a href=\"https://aprs.fi/#!call=".$myCallLinkUrl."*\" target=\"_blank\">dPRS</a>)</div></td>"; }
|
||||||
|
print "<td align=\"left\" width=\"100\">$yourCallHtml</td>";
|
||||||
|
print "<td align=\"left\" width=\"100\">$rpt1Html</td>";
|
||||||
|
print "<td align=\"left\" width=\"100\">$rpt2Html</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($LastHeardLog);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* D-Star reflector link manager — admin-only POST handler + form.
|
||||||
|
*
|
||||||
|
* Loaded inline by /index.php only on the admin path
|
||||||
|
* (`$_SERVER["PHP_SELF"] == "/admin/index.php"` gate at the top).
|
||||||
|
* Two responsibilities:
|
||||||
|
*
|
||||||
|
* 1. POST handler. Accepts form fields:
|
||||||
|
* - Link ("LINK" or "UNLINK")
|
||||||
|
* - RefName (7-char reflector name, e.g. REF001 / DCS007)
|
||||||
|
* - Letter (single-letter reflector module, A-Z)
|
||||||
|
* - Module (8-char repeater callsign+band, e.g. "MW0MWZ B")
|
||||||
|
* Each is validated by a preg_match whitelist (unsetting on bad
|
||||||
|
* input) before being interpolated into a `sudo remotecontrold ...`
|
||||||
|
* shell command. Refer to the security pass for hardening — the
|
||||||
|
* validation is the only barrier to command injection.
|
||||||
|
*
|
||||||
|
* 2. Form rendering. Drop-downs for Module (from repeaterBand1..4),
|
||||||
|
* RefName (DCS_Hosts.txt + DPlus_Hosts.txt + DExtra_Hosts.txt),
|
||||||
|
* Letter (A-Z), and a LINK / UNLINK radio pair. Uses the
|
||||||
|
* alternative `if (...): ... else: ?> ...HTML... <?php endif; ?>`
|
||||||
|
* template syntax — preserve the colons.
|
||||||
|
*
|
||||||
|
* Also surfaces a "PiStar-Keeper Logbook" panel below the form when
|
||||||
|
* /usr/local/sbin/pistar-keeper is running (last 5 entries from
|
||||||
|
* /var/pistar-keeper/pistar-keeper.log + a download link).
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") {
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
|
||||||
|
if (!empty($_POST) && isset($_POST["dstrMgrSubmit"])):
|
||||||
|
//if (!empty($_POST)):
|
||||||
|
if (preg_match('/[^A-Z]/',$_POST["Link"])) { unset ($_POST["Link"]);}
|
||||||
|
if ($_POST["Link"] == "LINK") {
|
||||||
|
if (preg_match('/[^A-Za-z0-9 ]/',$_POST["RefName"])) { unset ($_POST["RefName"]);}
|
||||||
|
if (preg_match('/[^A-Z]/',$_POST["Letter"])) { unset ($_POST["Letter"]);}
|
||||||
|
if (preg_match('/[^A-Z0-9 ]/',$_POST["Module"])) { unset ($_POST["Module"]);}
|
||||||
|
}
|
||||||
|
if ($_POST["Link"] == "UNLINK") {
|
||||||
|
if (preg_match('/[^A-Z0-9 ]/',$_POST["Module"])) { unset ($_POST["Module"]);}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_POST["RefName"]) || empty($_POST["Letter"]) || empty($_POST["Module"])) { echo "Somthing wrong with your input, try again";}
|
||||||
|
|
||||||
|
|
||||||
|
else {
|
||||||
|
if (strlen($_POST["RefName"]) != 7) {
|
||||||
|
$targetRef = str_pad($_POST["RefName"], 7, " ");
|
||||||
|
} else {
|
||||||
|
$targetRef = $_POST["RefName"];
|
||||||
|
}
|
||||||
|
$targetRef = $targetRef.$_POST["Letter"];
|
||||||
|
$targetRef = strtoupper($targetRef);
|
||||||
|
$module = $_POST["Module"];
|
||||||
|
|
||||||
|
if (strlen($module) != 8) { //Fix the length of the module information
|
||||||
|
$moduleFixedCs = strlen($module) - 1; //Length of the string, -1
|
||||||
|
$moduleFixedBand = substr($module, -1); //Single Band Letter in the 8th position
|
||||||
|
$moduleFixedCallPad = str_pad(substr($module, 0, $moduleFixedCs), 7); //Pad the callsign area to 7 chars
|
||||||
|
$module = $moduleFixedCallPad.$moduleFixedBand; //Re add the band information
|
||||||
|
};
|
||||||
|
|
||||||
|
$unlinkCommand = "sudo remotecontrold \"".$module."\" unlink";
|
||||||
|
$linkCommand = "sudo remotecontrold \"".$module."\" link never \"".$targetRef."\"";
|
||||||
|
|
||||||
|
if ($module != $targetRef && $_POST["Link"] == "LINK") { // Sanity check that we are not connecting to ourself
|
||||||
|
echo "<b>D-Star Link Manager</b>\n";
|
||||||
|
echo "<table>\n<tr><th>Command Output</th></tr>\n<tr><td>";
|
||||||
|
echo htmlspecialchars((string)exec($linkCommand), ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "</td></tr>\n</table>\n";
|
||||||
|
}
|
||||||
|
if ($module == $targetRef && $_POST["Link"] == "LINK") { // Sanity Check Failed
|
||||||
|
echo "<b>D-Star Link Manager</b>\n";
|
||||||
|
echo "<table>\n<tr><th>Command Output</th></tr>\n<tr><td>";
|
||||||
|
echo "Cannot link to myself - Aborting link request!";
|
||||||
|
echo "</td></tr>\n</table>\n";
|
||||||
|
}
|
||||||
|
if ($_POST["Link"] == "UNLINK") { // Allow Unlink no matter what
|
||||||
|
echo "<b>D-Star Link Manager</b>\n";
|
||||||
|
echo "<table>\n<tr><th>Command Output</th></tr>\n<tr><td>";
|
||||||
|
echo htmlspecialchars((string)exec($unlinkCommand), ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "</td></tr>\n</table>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($_POST);
|
||||||
|
echo '<script type="text/javascript">setTimeout(function() { window.location=window.location;},2000);</script>';
|
||||||
|
|
||||||
|
else: ?>
|
||||||
|
<b><?php echo $lang['d-star_link_manager'];?></b>
|
||||||
|
<form action="//<?php echo htmlentities($_SERVER['HTTP_HOST']).htmlentities($_SERVER['PHP_SELF']); ?>" method="post">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th width="150"><a class="tooltip" href="#">Radio Module<span><b>Radio Module</b></span></a></th>
|
||||||
|
<th width="180"><a class="tooltip" href="#">Reflector<span><b>Reflector</b></span></a></th>
|
||||||
|
<th width="150"><a class="tooltip" href="#">Link / Un-Link<span><b>Link / Un-Link</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#">Action<span><b>Action</b></span></a></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<select name="Module">
|
||||||
|
<?php
|
||||||
|
$ci = 0;
|
||||||
|
for($i = 1;$i < 5; $i++){
|
||||||
|
$param="repeaterBand" . $i;
|
||||||
|
if((isset($configs[$param])) && strlen($configs[$param]) == 1) {
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
$module = $configs[$param];
|
||||||
|
$rcall = sprintf("%-7.7s%-1.1s",$MYCALL,$module);
|
||||||
|
$param="repeaterCall" . $i;
|
||||||
|
if(isset($configs[$param])) { $rptrcall=sprintf("%-7.7s%-1.1s",$configs[$param],$module); } else { $rptrcall = $rcall;}
|
||||||
|
print "<option>$rptrcall</option>\n";
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="RefName"
|
||||||
|
onchange="if (this.options[this.selectedIndex].value == 'customOption') {
|
||||||
|
toggleField(this,this.nextSibling);
|
||||||
|
this.selectedIndex='0';
|
||||||
|
} ">
|
||||||
|
<?php
|
||||||
|
$dcsFile = fopen("/usr/local/etc/DCS_Hosts.txt", "r");
|
||||||
|
$dplusFile = fopen("/usr/local/etc/DPlus_Hosts.txt", "r");
|
||||||
|
$dextraFile = fopen("/usr/local/etc/DExtra_Hosts.txt", "r");
|
||||||
|
|
||||||
|
echo " <option value=\"".substr($configs['reflector1'], 0, 6)."\" selected=\"selected\">".substr($configs['reflector1'], 0, 6)."</option>\n";
|
||||||
|
echo " <option value=\"customOption\">Text Entry</option>\n";
|
||||||
|
|
||||||
|
while (!feof($dcsFile)) {
|
||||||
|
$dcsLine = fgets($dcsFile);
|
||||||
|
if (strpos($dcsLine, 'DCS') !== FALSE && strpos($dcsLine, '#') === FALSE) {
|
||||||
|
echo " <option value=\"".substr($dcsLine, 0, 6)."\">".substr($dcsLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
if (strpos($dcsLine, 'XLX') !== FALSE && strpos($dcsLine, '#') === FALSE) {
|
||||||
|
echo " <option value=\"".substr($dcsLine, 0, 6)."\">".substr($dcsLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($dcsFile);
|
||||||
|
|
||||||
|
while (!feof($dplusFile)) {
|
||||||
|
$dplusLine = fgets($dplusFile);
|
||||||
|
if (strpos($dplusLine, 'REF') !== FALSE && strpos($dplusLine, '#') === FALSE) {
|
||||||
|
echo " <option value=\"".substr($dplusLine, 0, 6)."\">".substr($dplusLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
if (strpos($dplusLine, 'XRF') !== FALSE && strpos($dplusLine, '#') === FALSE) {
|
||||||
|
echo " <option value=\"".substr($dplusLine, 0, 6)."\">".substr($dplusLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($dplusFile);
|
||||||
|
|
||||||
|
while (!feof($dextraFile)) {
|
||||||
|
$dextraLine = fgets($dextraFile);
|
||||||
|
if (strpos($dextraLine, 'XRF') !== FALSE && strpos($dextraLine, '#') === FALSE)
|
||||||
|
echo " <option value=\"".substr($dextraLine, 0, 6)."\">".substr($dextraLine, 0, 6)."</option>\n";
|
||||||
|
}
|
||||||
|
fclose($dextraFile);
|
||||||
|
|
||||||
|
?>
|
||||||
|
</select><input name="RefName" style="display:none;" disabled="disabled" type="text" size="7" maxlength="7"
|
||||||
|
onblur="if(this.value==''){toggleField(this,this.previousSibling);}" />
|
||||||
|
<select name="Letter">
|
||||||
|
<?php echo " <option value=\"".substr($configs['reflector1'], 7)."\" selected=\"selected\">".substr($configs['reflector1'], 7)."</option>\n"; ?>
|
||||||
|
<option>A</option>
|
||||||
|
<option>B</option>
|
||||||
|
<option>C</option>
|
||||||
|
<option>D</option>
|
||||||
|
<option>E</option>
|
||||||
|
<option>F</option>
|
||||||
|
<option>G</option>
|
||||||
|
<option>H</option>
|
||||||
|
<option>I</option>
|
||||||
|
<option>J</option>
|
||||||
|
<option>K</option>
|
||||||
|
<option>L</option>
|
||||||
|
<option>M</option>
|
||||||
|
<option>N</option>
|
||||||
|
<option>O</option>
|
||||||
|
<option>P</option>
|
||||||
|
<option>Q</option>
|
||||||
|
<option>R</option>
|
||||||
|
<option>S</option>
|
||||||
|
<option>T</option>
|
||||||
|
<option>U</option>
|
||||||
|
<option>V</option>
|
||||||
|
<option>W</option>
|
||||||
|
<option>X</option>
|
||||||
|
<option>Y</option>
|
||||||
|
<option>Z</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="Link" value="LINK" checked="checked" />Link
|
||||||
|
<input type="radio" name="Link" value="UNLINK" />UnLink
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="submit" name="dstrMgrSubmit" value="Request Change" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
exec ("pgrep pistar-keeper", $pids);
|
||||||
|
if (!empty($pids))
|
||||||
|
{
|
||||||
|
echo "<br />\n";
|
||||||
|
echo "<b>PiStar-Keeper Logbook</b><input type=button onClick=\"location.href='/admin/pistar-keeper-download.php'\" value=\"Download Logbook\">\n";
|
||||||
|
echo "<table>\n";
|
||||||
|
echo " <tr>\n";
|
||||||
|
echo " <th><a class=\"tooltip\" href=\"#\">PiStar-Keeper Log Entries (UTC)<span><b>PiStar-Keeper Log Entries (UTC)</b></span></th>\n";
|
||||||
|
echo " </tr>\n";
|
||||||
|
|
||||||
|
exec ("tail -n 5 /var/pistar-keeper/pistar-keeper.log", $lines);
|
||||||
|
$counter = 0;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
echo "<tr><td align=\"left\">".$lines[$counter]."</td></tr>\n";
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "</table>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Local TX list (D-Star repeater mode) — recent local transmissions
|
||||||
|
* the connected D-Star repeater has worked.
|
||||||
|
*
|
||||||
|
* AJAX-loaded partial; refreshed every ~3 seconds by /index.php in
|
||||||
|
* dstarrepeater mode. Same layout as last_herd.php (time / callsign /
|
||||||
|
* target / Rpt1 / Rpt2) but filters Headers.log by "Repeater header"
|
||||||
|
* lines so it only surfaces RF traffic, not network-side activity.
|
||||||
|
*
|
||||||
|
* Data flow: shells out to grep|sort against /var/log/pi-star/Headers.log
|
||||||
|
* to build /tmp/worked.log, then regex-parses each line. Callsign links
|
||||||
|
* use the operator's chosen lookup service (RadioID or QRZ from
|
||||||
|
* /etc/pistar-css.ini's [Lookup] Service key) plus aprs.fi for dPRS.
|
||||||
|
*
|
||||||
|
* The "Headers.log sample" comment block below documents the exact line
|
||||||
|
* shape the regex targets — preserve those examples verbatim.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
// AJAX-loaded partial — embeddable variant only. See the note in
|
||||||
|
// mmdvmhost/lh.php for why the historical double-call was wrong.
|
||||||
|
setEmbeddableSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
|
||||||
|
// Check if the config file exists
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
// Use the values from the file
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile,'r')) { $piStarCss = parse_ini_file($piStarCssFile, true); }
|
||||||
|
|
||||||
|
// Set the Values from the config file
|
||||||
|
if (isset($piStarCss['Lookup']['Service'])) { $callsignLookupSvc = $piStarCss['Lookup']['Service']; } // Lookup Service "QRZ" or "RadioID"
|
||||||
|
else { $callsignLookupSvc = "RadioID"; } // Set the default if its missing // Set the default if its missing
|
||||||
|
} else {
|
||||||
|
// Default values
|
||||||
|
$callsignLookupSvc = "RadioID";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety net
|
||||||
|
if (($callsignLookupSvc != "RadioID") && ($callsignLookupSvc != "QRZ")) { $callsignLookupSvc = "RadioID"; }
|
||||||
|
|
||||||
|
// Setup the URL(s)
|
||||||
|
if ($callsignLookupSvc == "RadioID") { $callsignLookupUrl = "https://database.radioid.net/database/view?callsign="; }
|
||||||
|
if ($callsignLookupSvc == "QRZ") { $callsignLookupUrl = "https://www.qrz.com/db/"; }
|
||||||
|
|
||||||
|
?>
|
||||||
|
<b><?php echo $lang['local_tx_list'];?></b>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['time'];?> (<?php echo date('T')?>)</a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['callsign'];?></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['target'];?></a></th>
|
||||||
|
<th><a class="tooltip" href="#">RPT 1</a></th>
|
||||||
|
<th><a class="tooltip" href="#">RPT 2</a></th>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
// Headers.log sample:
|
||||||
|
// 0000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122
|
||||||
|
// 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
|
||||||
|
// 2012-05-29 20:31:53: Repeater header - My: PE1AGO /HANS Your: CQCQCQ Rpt1: PI1DEC B Rpt2: PI1DEC G Flags: 00 00 00
|
||||||
|
//
|
||||||
|
exec('(grep "Repeater header" '.$hdrLogPath.'|sort -r -k7,7|sort -u -k7,8|sort -r >/tmp/worked.log) 2>&1 &');
|
||||||
|
$ci = 0;
|
||||||
|
if ($WorkedLog = fopen("/tmp/worked.log",'r')) {
|
||||||
|
while ($linkLine = fgets($WorkedLog)) {
|
||||||
|
if(preg_match_all('/^(.{19}).*My: (.*).*Your: (.*).*Rpt1: (.*).*Rpt2: (.*).*Flags: (.*)$/',$linkLine,$linx) > 0){
|
||||||
|
$ci++;
|
||||||
|
if($ci > 1) { $ci = 0; }
|
||||||
|
$QSODate = date("d-M-Y H:i:s", strtotime(substr($linx[1][0],0,19)));
|
||||||
|
|
||||||
|
// Same normalisation pattern as dstarrepeater/last_herd.php —
|
||||||
|
// see the note in that file for the rationale.
|
||||||
|
$myCallRaw = str_replace(' ', '', substr($linx[2][0],0,8));
|
||||||
|
$myCallHtml = htmlspecialchars($myCallRaw, ENT_QUOTES, 'UTF-8');
|
||||||
|
$myCallLink = strtok(substr($linx[2][0],0,8), " ");
|
||||||
|
$myCallLinkUrl = rawurlencode((string)$myCallLink);
|
||||||
|
$myIdHtml = htmlspecialchars(str_replace(' ', '', substr($linx[2][0],9,4)), ENT_QUOTES, 'UTF-8');
|
||||||
|
$yourCallHtml = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[3][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
$rpt1Html = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[4][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
$rpt2Html = str_replace(' ', ' ',
|
||||||
|
htmlspecialchars(substr($linx[5][0],0,8), ENT_QUOTES, 'UTF-8'));
|
||||||
|
|
||||||
|
$utc_time = $QSODate;
|
||||||
|
$utc_tz = new DateTimeZone('UTC');
|
||||||
|
$local_tz = new DateTimeZone(date_default_timezone_get ());
|
||||||
|
$dt = new DateTime($utc_time, $utc_tz);
|
||||||
|
$dt->setTimeZone($local_tz);
|
||||||
|
$local_time = $dt->format('H:i:s M jS');
|
||||||
|
print "<tr>";
|
||||||
|
print "<td align=\"left\">$local_time</td>";
|
||||||
|
print "<td align=\"left\" width=\"180\"><div style=\"float:left;\"><a href=\"".$callsignLookupUrl.$myCallLinkUrl."\" target=\"_blank\">$myCallHtml</a>";
|
||||||
|
if($myIdHtml !== '') { print "/$myIdHtml</div> <div style=\"text-align:right;\">(<a href=\"https://aprs.fi/#!call=".$myCallLinkUrl."*\" target=\"_blank\">dPRS</a>)</div></td>"; }
|
||||||
|
else { print "</div> <div style=\"text-align:right;\">(<a href=\"https://aprs.fi/#!call=".$myCallLinkUrl."*\" target=\"_blank\">dPRS</a>)</div></td>"; }
|
||||||
|
print "<td align=\"left\" width=\"100\">$yourCallHtml</td>";
|
||||||
|
print "<td align=\"left\" width=\"100\">$rpt1Html</td>";
|
||||||
|
print "<td align=\"left\" width=\"100\">$rpt2Html</td>";
|
||||||
|
print "</tr>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($WorkedLog);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* System info partial — hostname, kernel, platform, CPU load/temp, service
|
||||||
|
* status grid (MMDVMHost / DStarRepeater / ircDDBGateway / TimeServer /
|
||||||
|
* PiStar-Watchdog / PiStar-Remote).
|
||||||
|
*
|
||||||
|
* Loaded inline by /index.php on the admin path and AJAX-refreshed every
|
||||||
|
* 15 seconds via $("#sysInfo").load(...). Used in BOTH MMDVMHost and
|
||||||
|
* DStarRepeater modes (same partial, gated by /index.php).
|
||||||
|
*
|
||||||
|
* Served two ways from /index.php: an inline `include` for the initial
|
||||||
|
* page render, then AJAX `$("#sysInfo").load(...)` for refreshes. On
|
||||||
|
* the inline path the parent has already emitted full security headers
|
||||||
|
* (and the headers_sent() guard inside makes our call idempotent); on
|
||||||
|
* the AJAX path the response is XHR — X-Frame-Options / frame-ancestors
|
||||||
|
* apply to iframe ancestry, not XHR, so the embeddable variant is the
|
||||||
|
* correct choice in both directions.
|
||||||
|
*
|
||||||
|
* Reads /etc/ircddbgateway (flat key=value via the hand-rolled parser)
|
||||||
|
* to surface the gateway callsign in the hardware-info table.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||||
|
setEmbeddableSecurityHeaders();
|
||||||
|
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/ircddblocal.php';
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/config/language.php'; // Translation Code
|
||||||
|
include_once $_SERVER['DOCUMENT_ROOT'].'/mmdvmhost/tools.php';
|
||||||
|
$configs = array();
|
||||||
|
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev="20141101";
|
||||||
|
$MYCALL=strtoupper($callsign);
|
||||||
|
?>
|
||||||
|
<?php
|
||||||
|
$cpuLoad = sys_getloadavg();
|
||||||
|
$cpuTempCRaw = exec('cat /sys/class/thermal/thermal_zone0/temp');
|
||||||
|
if ($cpuTempCRaw > 1000) { $cpuTempC = round($cpuTempCRaw / 1000, 1); } else { $cpuTempC = round($cpuTempCRaw, 1); }
|
||||||
|
$cpuTempF = round(+$cpuTempC * 9 / 5 + 32, 1);
|
||||||
|
if ($cpuTempC < 50) { $cpuTempHTML = "<td style=\"background: #1d1\">".$cpuTempC."°C / ".$cpuTempF."°F</td>\n"; }
|
||||||
|
if ($cpuTempC >= 50) { $cpuTempHTML = "<td style=\"background: #fa0\">".$cpuTempC."°C / ".$cpuTempF."°F</td>\n"; }
|
||||||
|
if ($cpuTempC >= 69) { $cpuTempHTML = "<td style=\"background: #f00\">".$cpuTempC."°C / ".$cpuTempF."°F</td>\n"; }
|
||||||
|
?>
|
||||||
|
<b><?php echo $lang['hardware_info'];?></b>
|
||||||
|
<table style="table-layout: fixed;">
|
||||||
|
<tr>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['hostname'];?><br /><span><b>System IP Address:<br /><?php echo str_replace(',', ',<br />', exec('hostname -I'));?></b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['kernel'];?><span><b>Release</b></span></a></th>
|
||||||
|
<th colspan="2"><a class="tooltip" href="#"><?php echo $lang['platform'];?><span><b>Uptime:<br /><?php echo str_replace(',', ',<br />', exec('uptime -p'));?></b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['cpu_load'];?><span><b>CPU Load</b></span></a></th>
|
||||||
|
<th><a class="tooltip" href="#"><?php echo $lang['cpu_temp'];?><span><b>CPU Temp</b></span></a></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php $h = php_uname('n'); if (strlen($h) >= 16) { $h = substr($h, 0, 14) . '..'; } echo htmlspecialchars((string)$h, ENT_QUOTES, 'UTF-8'); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)php_uname('r'), ENT_QUOTES, 'UTF-8');?></td>
|
||||||
|
<td colspan="2"><?php echo htmlspecialchars((string)exec('/usr/local/bin/platformDetect.sh'), ENT_QUOTES, 'UTF-8');?></td>
|
||||||
|
<td><?php echo number_format($cpuLoad[0],2);?> / <?php echo number_format($cpuLoad[1],2);?> / <?php echo number_format($cpuLoad[2],2);?></td>
|
||||||
|
<?php echo $cpuTempHTML; ?>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th colspan="6"><?php echo $lang['service_status'];?></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('MMDVMHost')) { echo "1d1"; } else { echo "b55"; } ?>">MMDVMHost</td>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('dstarrepeaterd')) { echo "1d1"; } else { echo "b55"; } ?>">DStarRepeater</td>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('ircddbgatewayd')) { echo "1d1"; } else { echo "b55"; } ?>">ircDDBGateway</td>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('timeserverd')) { echo "1d1"; } else { echo "b55"; } ?>">TimeServer</td>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('/usr/local/sbin/pistar-watchdog',true)) { echo "1d1"; } else { echo "b55"; } ?>">PiStar-Watchdog</td>
|
||||||
|
<td style="background: #<?php if (isProcessRunning('/usr/local/sbin/pistar-remote',true)) { echo "1d1"; } else { echo "b55"; } ?>">PiStar-Remote</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
function toggleField(hideObj,showObj) {
|
||||||
|
hideObj.disabled=true;
|
||||||
|
hideObj.style.display='none';
|
||||||
|
showObj.disabled=false;
|
||||||
|
showObj.style.display='inline';
|
||||||
|
showObj.focus();
|
||||||
|
}
|
||||||
|
function checkPass(){ //used in confirm matching password entries
|
||||||
|
var pass1 = document.getElementById('pass1');
|
||||||
|
var pass2 = document.getElementById('pass2');
|
||||||
|
var goodColor = "#66cc66";
|
||||||
|
var badColor = "#ff6666";
|
||||||
|
if((pass1.value != '') && (pass1.value == pass2.value)){
|
||||||
|
pass2.style.backgroundColor = goodColor;
|
||||||
|
document.getElementById('submitpwd').removeAttribute("disabled");
|
||||||
|
}else{
|
||||||
|
pass2.style.backgroundColor = badColor;
|
||||||
|
document.getElementById('submitpwd').setAttribute("disabled","disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function checkPsk() {
|
||||||
|
if(psk1.value.length > 0 && psk1.value.length < 8) {
|
||||||
|
psk1.style.background='#ff6666';
|
||||||
|
} else {
|
||||||
|
psk1.style.background='#66cc66';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function checkPskMatch(){ //used in confirm matching psk entries
|
||||||
|
var psk1 = document.getElementById('psk1');
|
||||||
|
var psk2 = document.getElementById('psk2');
|
||||||
|
var goodColor = "#66cc66";
|
||||||
|
var badColor = "#ff6666";
|
||||||
|
if((psk1.value != '') && (psk1.value == psk2.value)){
|
||||||
|
psk2.style.backgroundColor = goodColor;
|
||||||
|
document.getElementById('submitpsk').removeAttribute("disabled");
|
||||||
|
}else{
|
||||||
|
psk2.style.backgroundColor = badColor;
|
||||||
|
document.getElementById('submitpsk').setAttribute("disabled","disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function checkFrequency(){
|
||||||
|
// Set the colours
|
||||||
|
var goodColor = "#66cc66";
|
||||||
|
var badColor = "#ff6666";
|
||||||
|
// Get the objects from the config page
|
||||||
|
var freqTRX = document.getElementById('confFREQ');
|
||||||
|
var freqRX = document.getElementById('confFREQrx');
|
||||||
|
var freqTX = document.getElementById('confFREQtx');
|
||||||
|
var freqPOCSAG = document.getElementById('pocsagFrequency');
|
||||||
|
if(freqTRX){
|
||||||
|
confFREQ.style.backgroundColor = badColor; // Set to bad colour first, then check
|
||||||
|
var intFreqTRX = parseFloat(freqTRX.value); // Swap to float
|
||||||
|
// TRX Good
|
||||||
|
if (144 <= intFreqTRX && intFreqTRX <= 148) { confFREQ.style.backgroundColor = goodColor; }
|
||||||
|
if (220 <= intFreqTRX && intFreqTRX <= 225) { confFREQ.style.backgroundColor = goodColor; }
|
||||||
|
if (420 <= intFreqTRX && intFreqTRX <= 450) { confFREQ.style.backgroundColor = goodColor; }
|
||||||
|
if (842 <= intFreqTRX && intFreqTRX <= 950) { confFREQ.style.backgroundColor = goodColor; }
|
||||||
|
if (1240 <= intFreqTRX && intFreqTRX <= 1300) { confFREQ.style.backgroundColor = goodColor; }
|
||||||
|
// TRX Bad
|
||||||
|
if (145.8 <= intFreqTRX && intFreqTRX <= 146) { confFREQ.style.backgroundColor = badColor; }
|
||||||
|
if (435 <= intFreqTRX && intFreqTRX <= 438) { confFREQ.style.backgroundColor = badColor; }
|
||||||
|
if (1260 <= intFreqTRX && intFreqTRX <= 1270) { confFREQ.style.backgroundColor = badColor; }
|
||||||
|
}
|
||||||
|
if(freqRX){
|
||||||
|
confFREQrx.style.backgroundColor = badColor; // Set to bad colour first, then check
|
||||||
|
var intFreqRX = parseFloat(freqRX.value); // Swap to float
|
||||||
|
// RX Good
|
||||||
|
if (144 <= intFreqRX && intFreqRX <= 148) { confFREQrx.style.backgroundColor = goodColor; }
|
||||||
|
if (220 <= intFreqRX && intFreqRX <= 225) { confFREQrx.style.backgroundColor = goodColor; }
|
||||||
|
if (420 <= intFreqRX && intFreqRX <= 450) { confFREQrx.style.backgroundColor = goodColor; }
|
||||||
|
if (842 <= intFreqRX && intFreqRX <= 950) { confFREQrx.style.backgroundColor = goodColor; }
|
||||||
|
if (1240 <= intFreqRX && intFreqRX <= 1300) { confFREQrx.style.backgroundColor = goodColor; }
|
||||||
|
// RX Bad
|
||||||
|
if (145.8 <= intFreqRX && intFreqRX <= 146) { confFREQrx.style.backgroundColor = badColor; }
|
||||||
|
if (435 <= intFreqRX && intFreqRX <= 438) { confFREQrx.style.backgroundColor = badColor; }
|
||||||
|
if (1260 <= intFreqRX && intFreqRX <= 1270) { confFREQrx.style.backgroundColor = badColor; }
|
||||||
|
}
|
||||||
|
if(freqTX){
|
||||||
|
confFREQtx.style.backgroundColor = badColor; // Set to bad colour first, then check
|
||||||
|
var intFreqTX = parseFloat(freqTX.value); // Swap to float
|
||||||
|
// TX Good
|
||||||
|
if (144 <= intFreqTX && intFreqTX <= 148) { confFREQtx.style.backgroundColor = goodColor; }
|
||||||
|
if (220 <= intFreqTX && intFreqTX <= 225) { confFREQtx.style.backgroundColor = goodColor; }
|
||||||
|
if (420 <= intFreqTX && intFreqTX <= 450) { confFREQtx.style.backgroundColor = goodColor; }
|
||||||
|
if (842 <= intFreqTX && intFreqTX <= 950) { confFREQtx.style.backgroundColor = goodColor; }
|
||||||
|
if (1240 <= intFreqTX && intFreqTX <= 1300) { confFREQtx.style.backgroundColor = goodColor; }
|
||||||
|
// TX Bad
|
||||||
|
if (145.8 <= intFreqTX && intFreqTX <= 146) { confFREQtx.style.backgroundColor = badColor; }
|
||||||
|
if (435 <= intFreqTX && intFreqTX <= 438) { confFREQtx.style.backgroundColor = badColor; }
|
||||||
|
if (1260 <= intFreqTX && intFreqTX <= 1270) { confFREQtx.style.backgroundColor = badColor; }
|
||||||
|
}
|
||||||
|
if(freqPOCSAG){
|
||||||
|
pocsagFrequency.style.backgroundColor = badColor; // Set to bad colour first, then check
|
||||||
|
var intFreqPOCSAG = parseFloat(freqPOCSAG.value); // Swap to float
|
||||||
|
// TX Good
|
||||||
|
if (144 <= intFreqPOCSAG && intFreqPOCSAG <= 148) { pocsagFrequency.style.backgroundColor = goodColor; }
|
||||||
|
if (220 <= intFreqPOCSAG && intFreqPOCSAG <= 225) { pocsagFrequency.style.backgroundColor = goodColor; }
|
||||||
|
if (420 <= intFreqPOCSAG && intFreqPOCSAG <= 450) { pocsagFrequency.style.backgroundColor = goodColor; }
|
||||||
|
if (842 <= intFreqPOCSAG && intFreqPOCSAG <= 950) { pocsagFrequency.style.backgroundColor = goodColor; }
|
||||||
|
// TX Bad
|
||||||
|
if (145.8 <= intFreqPOCSAG && intFreqPOCSAG <= 146) { pocsagFrequency.style.backgroundColor = badColor; }
|
||||||
|
if (435 <= intFreqPOCSAG && intFreqPOCSAG <= 438) { pocsagFrequency.style.backgroundColor = badColor; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleDMRCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmr').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmr').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-dmr').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmr').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-dmr').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmr').click(); }
|
||||||
|
}
|
||||||
|
function toggleDSTARCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dstar').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dstar').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-dstar').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dstar').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-dstar').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dstar').click(); }
|
||||||
|
}
|
||||||
|
function toggleYSFCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-ysf').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-ysf').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-ysf').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-ysf').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-ysf').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-ysf').click(); }
|
||||||
|
}
|
||||||
|
function toggleP25Checkbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-p25').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-p25').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-p25').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-p25').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-p25').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-p25').click(); }
|
||||||
|
}
|
||||||
|
function toggleNXDNCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-nxdn').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-nxdn').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-nxdn').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-nxdn').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-nxdn').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-nxdn').click(); }
|
||||||
|
}
|
||||||
|
function toggleM17Checkbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-m17').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-m17').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-m17').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-m17').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-m17').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-m17').click(); }
|
||||||
|
}
|
||||||
|
function toggleYSF2DMRCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-ysf2dmr').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-ysf2dmr').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-ysf2dmr').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-ysf2dmr').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-ysf2dmr').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-ysf2dmr').click(); }
|
||||||
|
}
|
||||||
|
function toggleYSF2NXDNCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-ysf2nxdn').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-ysf2nxdn').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-ysf2nxdn').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-ysf2nxdn').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-ysf2nxdn').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-ysf2nxdn').click(); }
|
||||||
|
}
|
||||||
|
function toggleYSF2P25Checkbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-ysf2p25').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-ysf2p25').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-ysf2p25').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-ysf2p25').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-ysf2p25').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-ysf2p25').click(); }
|
||||||
|
}
|
||||||
|
function toggleDMR2YSFCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmr2ysf').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmr2ysf').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-dmr2ysf').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmr2ysf').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-dmr2ysf').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmr2ysf').click(); }
|
||||||
|
}
|
||||||
|
function toggleDMR2NXDNCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmr2nxdn').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmr2nxdn').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-dmr2nxdn').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmr2nxdn').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-dmr2nxdn').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmr2nxdn').click(); }
|
||||||
|
}
|
||||||
|
function togglePOCSAGCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-pocsag').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-pocsag').setAttribute('aria-checked', "false");
|
||||||
|
//document.getElementById('toggle-pocsag').click();
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-pocsag').setAttribute('aria-checked', "true");
|
||||||
|
//document.getElementById('toggle-pocsag').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-pocsag').click(); }
|
||||||
|
}
|
||||||
|
function toggleDmrGatewayNet1EnCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmrGatewayNet1En').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayNet1En').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayNet1En').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmrGatewayNet1En').click(); }
|
||||||
|
}
|
||||||
|
function toggleDmrGatewayNet2EnCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmrGatewayNet2En').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayNet2En').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayNet2En').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmrGatewayNet2En').click(); }
|
||||||
|
}
|
||||||
|
function toggleDmrGatewayXlxEnCheckbox(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmrGatewayXlxEn').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayXlxEn').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmrGatewayXlxEn').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmrGatewayXlxEn').click(); }
|
||||||
|
}
|
||||||
|
function toggleDmrEmbeddedLCOnly(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmrEmbeddedLCOnly').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmrEmbeddedLCOnly').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmrEmbeddedLCOnly').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmrEmbeddedLCOnly').click(); }
|
||||||
|
}
|
||||||
|
function toggleDmrDumpTAData(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dmrDumpTAData').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dmrDumpTAData').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dmrDumpTAData').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dmrDumpTAData').click(); }
|
||||||
|
}
|
||||||
|
function toggleHostFilesYSFUpper(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-confHostFilesYSFUpper').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-confHostFilesYSFUpper').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-confHostFilesYSFUpper').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-confHostFilesYSFUpper').click(); }
|
||||||
|
}
|
||||||
|
function toggleWiresXCommandPassthrough(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-confWiresXCommandPassthrough').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-confWiresXCommandPassthrough').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-confWiresXCommandPassthrough').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-confWiresXCommandPassthrough').click(); }
|
||||||
|
}
|
||||||
|
function toggleDstarTimeAnnounce(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-timeAnnounce').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-timeAnnounce').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-timeAnnounce').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-timeAnnounce').click(); }
|
||||||
|
}
|
||||||
|
function toggleDstarDplusHostfiles(event) {
|
||||||
|
switch(document.getElementById('aria-toggle-dplusHostFiles').getAttribute('aria-checked')) {
|
||||||
|
case "true":
|
||||||
|
document.getElementById('aria-toggle-dplusHostFiles').setAttribute('aria-checked', "false");
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
document.getElementById('aria-toggle-dplusHostFiles').setAttribute('aria-checked', "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(event.keyCode == '32') { document.getElementById('aria-toggle-dplusHostFiles').click(); }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,330 @@
|
|||||||
|
<?php
|
||||||
|
require_once('config/version.php');
|
||||||
|
require_once('config/ircddblocal.php');
|
||||||
|
require_once('config/language.php');
|
||||||
|
$configs = array();
|
||||||
|
if ($configfile = fopen($gatewayConfigPath,'r')) {
|
||||||
|
while ($line = fgets($configfile)) {
|
||||||
|
list($key,$value) = preg_split('/=/',$line);
|
||||||
|
$value = trim(str_replace('"','',$value));
|
||||||
|
if ($key != 'ircddbPassword' && strlen($value) > 0)
|
||||||
|
$configs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$progname = basename($_SERVER['SCRIPT_FILENAME'],".php");
|
||||||
|
$rev=$version;
|
||||||
|
//$MYCALL=strtoupper($callsign);
|
||||||
|
$MYCALL=strtoupper($configs['gatewayCallsign']);
|
||||||
|
|
||||||
|
// Check if the config file exists
|
||||||
|
if (file_exists('/etc/pistar-css.ini')) {
|
||||||
|
$piStarCssFile = '/etc/pistar-css.ini';
|
||||||
|
if (fopen($piStarCssFile,'r')) { $piStarCss = parse_ini_file($piStarCssFile, true); }
|
||||||
|
if ($piStarCss['BannerH1']['Enabled']) {
|
||||||
|
$piStarCssBannerH1 = $piStarCss['BannerH1']['Text'];
|
||||||
|
}
|
||||||
|
if ($piStarCss['BannerExtText']['Enabled']) {
|
||||||
|
$piStarCssBannerExtTxt = $piStarCss['BannerExtText']['Text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load the Pi-Star Release file
|
||||||
|
$pistarReleaseConfig = '/etc/pistar-release';
|
||||||
|
$configPistarRelease = array();
|
||||||
|
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="robots" content="index" />
|
||||||
|
<meta name="robots" content="follow" />
|
||||||
|
<meta name="language" content="English" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<?php echo "<meta name=\"generator\" content=\"$progname $rev\" />\n"; ?>
|
||||||
|
<meta name="Author" content="Hans-J. Barthen (DL5DI), Kim Huebel (DG9VH) and Andy Taylor (MW0MWZ)" />
|
||||||
|
<meta name="Description" content="CDN Dashboard" />
|
||||||
|
<meta name="KeyWords" content="MW0MWZ,MMDVMHost,ircDDBGateway,D-Star,ircDDB,CDN,Blackwood,Wales,DL5DI,DG9VH" />
|
||||||
|
<meta http-equiv="cache-control" content="max-age=0" />
|
||||||
|
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="expires" content="0" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/nice-select.min.css?ver=<?php echo $configPistarRelease['Pi-Star']['Version']; ?>" />
|
||||||
|
<title><?php echo "$MYCALL"." - ".$lang['digital_voice']." ".$lang['dashboard'];?></title>
|
||||||
|
<?php include_once "config/browserdetect.php"; ?>
|
||||||
|
<script type="text/javascript" src="/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/functions.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$.ajaxSetup({ cache: false });
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div style="font-size: 8px; text-align: left; padding-left: 8px; float: left;">Hostname: <?php echo exec('cat /etc/hostname'); ?></div><div style="font-size: 8px; text-align: right; padding-right: 8px;">CDN:<?php echo $configPistarRelease['Pi-Star']['Version']?> / <?php echo $lang['dashboard'].": ".$version; ?></div>
|
||||||
|
<h1>CDN <?php echo $lang['digital_voice']." ".$lang['dashboard_for']." ".$MYCALL; ?></h1>
|
||||||
|
<?php if (isset($piStarCssBannerH1)) { echo "<h1>".$piStarCssBannerH1."</h1>\n"; } ?>
|
||||||
|
<?php if (isset($piStarCssBannerExtTxt)) { echo "<p style=\"text-align: center; color: #ffffff;\">".$piStarCssBannerExtTxt."</p>\n"; }?>
|
||||||
|
<?php $onAdminPage = ($_SERVER["PHP_SELF"] == "/admin/index.php"); ?>
|
||||||
|
<p style="padding-right: 5px; text-align: right; color: #ffffff;">
|
||||||
|
<a href="/" style="color: #ffffff;" class="<?php echo !$onAdminPage ? 'active' : ''; ?>"><?php echo $lang['dashboard'];?></a> |
|
||||||
|
<a href="/admin/" style="color: #ffffff;" class="<?php echo $onAdminPage ? 'active' : ''; ?>"><?php echo $lang['admin'];?></a> |
|
||||||
|
<?php if ($onAdminPage) {
|
||||||
|
echo ' <a href="/admin/live_modem_log.php" style="color: #ffffff;">'.$lang['live_logs'].'</a> |'."\n";
|
||||||
|
echo ' <a href="/admin/power.php" style="color: #ffffff;">'.$lang['power'].'</a> |'."\n";
|
||||||
|
} ?>
|
||||||
|
<a href="/admin/configure.php" style="color: #ffffff;"><?php echo $lang['configuration'];?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Output some default features
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") {
|
||||||
|
echo '<div class="contentwide">'."\n";
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadSysInfo(){'."\n";
|
||||||
|
echo ' $("#sysInfo").load("/dstarrepeater/system.php",function(){ setTimeout(reloadSysInfo,15000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadSysInfo,15000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="sysInfo">'."\n";
|
||||||
|
include 'dstarrepeater/system.php'; // Basic System Info
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo '</div>'."\n";
|
||||||
|
}
|
||||||
|
// First lets figure out if we are in MMDVMHost mode, or dstarrepeater mode;
|
||||||
|
if (file_exists('/etc/dstar-radio.mmdvmhost')) {
|
||||||
|
include 'config/config.php'; // MMDVMDash Config
|
||||||
|
include_once 'mmdvmhost/tools.php'; // MMDVMDash Tools
|
||||||
|
|
||||||
|
function getMMDVMConfigFileContent() {
|
||||||
|
// loads /etc/mmdvmhost into array for further use
|
||||||
|
$conf = array();
|
||||||
|
if ($configs = @fopen('/etc/mmdvmhost', 'r')) {
|
||||||
|
while ($config = fgets($configs)) {
|
||||||
|
array_push($conf, trim ( $config, " \t\n\r\0\x0B"));
|
||||||
|
}
|
||||||
|
fclose($configs);
|
||||||
|
}
|
||||||
|
return $conf;
|
||||||
|
}
|
||||||
|
$mmdvmconfigfile = getMMDVMConfigFileContent();
|
||||||
|
|
||||||
|
$dashboardRowOpen = true;
|
||||||
|
echo '<div class="dashboard-row">'."\n"; // Pairs .nav + .content so they stretch to equal height
|
||||||
|
echo '<div class="nav">'."\n"; // Start the Side Menu
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadRepeaterInfo(){'."\n";
|
||||||
|
echo ' $("#repeaterInfo").load("/mmdvmhost/repeaterinfo.php",function(){ setTimeout(reloadRepeaterInfo,1000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadRepeaterInfo,1000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="repeaterInfo">'."\n";
|
||||||
|
include 'mmdvmhost/repeaterinfo.php'; // MMDVMDash Repeater Info
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo '</div>'."\n";
|
||||||
|
|
||||||
|
echo '<div class="content">'."\n";
|
||||||
|
|
||||||
|
$testMMDVModeDSTARnet = getConfigItem("D-Star Network", "Enable", $mmdvmconfigs);
|
||||||
|
if ( $testMMDVModeDSTARnet == 1 ) { // If D-Star network is enabled, add these extra features.
|
||||||
|
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadrefLinks(){'."\n";
|
||||||
|
echo ' $("#refLinks").load("/dstarrepeater/active_reflector_links.php",function(){ setTimeout(reloadrefLinks,15000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadrefLinks,15000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="refLinks">'."\n";
|
||||||
|
include 'dstarrepeater/active_reflector_links.php'; // dstarrepeater gateway config
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo '<br />'."\n";
|
||||||
|
|
||||||
|
include 'dstarrepeater/link_manager.php'; // D-Star Link Manager
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadcssConnections(){'."\n";
|
||||||
|
echo ' $("#cssConnects").load("/dstarrepeater/css_connections.php",function(){ setTimeout(reloadcssConnections,15000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadcssConnections,15000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="cssConnects">'."\n";
|
||||||
|
include 'dstarrepeater/css_connections.php'; // dstarrepeater gateway config
|
||||||
|
echo '</div>'."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadbmConnections(){'."\n";
|
||||||
|
echo ' $("#bmConnects").load("/mmdvmhost/bm_links.php",function(){ setTimeout(reloadbmConnections,180000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadbmConnections,180000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="bmConnects">'."\n";
|
||||||
|
include 'mmdvmhost/bm_links.php'; // BM Links
|
||||||
|
echo '</div>'."\n";
|
||||||
|
}
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Options
|
||||||
|
include 'mmdvmhost/bm_manager.php'; // BM DMR Link Manager
|
||||||
|
}
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadtgifConnections(){'."\n";
|
||||||
|
echo ' $("#tgifConnects").load("/mmdvmhost/tgif_links.php",function(){ setTimeout(reloadtgifConnections,180000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadtgifConnections,180000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="tgifConnects">'."\n";
|
||||||
|
include 'mmdvmhost/tgif_links.php'; // TGIF Links
|
||||||
|
echo '</div>'."\n";
|
||||||
|
}
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Options
|
||||||
|
include 'mmdvmhost/tgif_manager.php'; // TGIF DMR Link Manager
|
||||||
|
}
|
||||||
|
$testMMDVModeYSFnet = getConfigItem("System Fusion Network", "Enable", $mmdvmconfigs);
|
||||||
|
if ( $testMMDVModeYSFnet == 1 ) { // If YSF network is enabled, add these extra features.
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
include 'mmdvmhost/ysf_manager.php'; // YSF Links
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$testMMDVModeP25net = getConfigItem("P25 Network", "Enable", $mmdvmconfigs);
|
||||||
|
if ( $testMMDVModeP25net == 1 ) { // If P25 network is enabled, add these extra features.
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
include 'mmdvmhost/p25_manager.php'; // P25 Links
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$testMMDVModeNXDNnet = getConfigItem("NXDN Network", "Enable", $mmdvmconfigs);
|
||||||
|
if ( $testMMDVModeNXDNnet == 1 ) { // If NXDN network is enabled, add these extra features.
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
include 'mmdvmhost/nxdn_manager.php'; // NXDN Links
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$testMMDVModeM17net = getConfigItem("M17 Network", "Enable", $mmdvmconfigs);
|
||||||
|
if ( $testMMDVModeM17net == 1 ) { // If NXDN network is enabled, add these extra features.
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Option
|
||||||
|
include 'mmdvmhost/m17_manager.php'; // M17 Links
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadLocalTx(){'."\n";
|
||||||
|
echo ' $("#localTxs").load("/mmdvmhost/localtx.php",function(){ setTimeout(reloadLocalTx,1500) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadLocalTx,1500);'."\n";
|
||||||
|
echo 'function reloadLastHerd(){'."\n";
|
||||||
|
echo ' $("#lastHerd").load("/mmdvmhost/lh.php",function(){ setTimeout(reloadLastHerd,1500) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadLastHerd,1500);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="lastHerd">'."\n";
|
||||||
|
include 'mmdvmhost/lh.php'; // MMDVMDash Last Herd
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
echo '<div id="localTxs">'."\n";
|
||||||
|
include 'mmdvmhost/localtx.php'; // MMDVMDash Local Trasmissions
|
||||||
|
echo '</div>'."\n";
|
||||||
|
|
||||||
|
// If POCSAG is enabled, show the information pannel
|
||||||
|
$testMMDVModePOCSAG = getConfigItem("POCSAG Network", "Enable", $mmdvmconfigfile);
|
||||||
|
if ( $testMMDVModePOCSAG == 1 ) {
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadPages(){'."\n";
|
||||||
|
echo ' $("#Pages").load("/mmdvmhost/pages.php",function(){ setTimeout(reloadPages,5000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadPages,5000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
echo '<div id="Pages">'."\n";
|
||||||
|
include 'mmdvmhost/pages.php'; // POCSAG Messages
|
||||||
|
echo '</div>'."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif (file_exists('/etc/dstar-radio.dstarrepeater')) {
|
||||||
|
echo '<div class="contentwide">'."\n";
|
||||||
|
include 'dstarrepeater/gateway_software_config.php'; // dstarrepeater gateway config
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadrefLinks(){'."\n";
|
||||||
|
echo ' $("#refLinks").load("/dstarrepeater/active_reflector_links.php",function(){ setTimeout(reloadrefLinks,15000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadrefLinks,15000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<br />'."\n";
|
||||||
|
echo '<div id="refLinks">'."\n";
|
||||||
|
include 'dstarrepeater/active_reflector_links.php'; // dstarrepeater gateway config
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo '<br />'."\n";
|
||||||
|
if ($_SERVER["PHP_SELF"] == "/admin/index.php") { // Admin Only Options
|
||||||
|
include 'dstarrepeater/link_manager.php'; // D-Star Link Manager
|
||||||
|
echo "<br />\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadcssConnections(){'."\n";
|
||||||
|
echo ' $("#cssConnects").load("/dstarrepeater/css_connections.php",function(){ setTimeout(reloadcssConnections,15000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadcssConnections,15000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="cssConnects">'."\n";
|
||||||
|
include 'dstarrepeater/css_connections.php'; // dstarrepeater gateway config
|
||||||
|
echo '</div>'."\n";
|
||||||
|
|
||||||
|
echo '<script type="text/javascript">'."\n";
|
||||||
|
echo 'function reloadLocalTx(){'."\n";
|
||||||
|
echo ' $("#localTx").load("/dstarrepeater/local_tx.php",function(){ setTimeout(reloadLocalTx,3000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadLocalTx,3000);'."\n";
|
||||||
|
echo 'function reloadLastHerd(){'."\n";
|
||||||
|
echo ' $("#lh").load("/dstarrepeater/last_herd.php",function(){ setTimeout(reloadLastHerd,3000) });'."\n";
|
||||||
|
echo '}'."\n";
|
||||||
|
echo 'setTimeout(reloadLastHerd,3000);'."\n";
|
||||||
|
echo '$(window).trigger(\'resize\');'."\n";
|
||||||
|
echo '</script>'."\n";
|
||||||
|
echo '<div id="lh">'."\n";
|
||||||
|
include 'dstarrepeater/last_herd.php'; //dstarrepeater Last Herd
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo "<br />\n";
|
||||||
|
echo '<div id="localTx">'."\n";
|
||||||
|
include 'dstarrepeater/local_tx.php'; //dstarrepeater Local Transmissions
|
||||||
|
echo '</div>'."\n";
|
||||||
|
echo '<br />'."\n";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo '<div class="contentwide">'."\n";
|
||||||
|
//We dont know what mode we are in - fail...
|
||||||
|
echo "<H1>未定义工作模式...</H1>\n";
|
||||||
|
echo '<script type="text/javascript">setTimeout(function() { window.location="/admin/configure.php";},10000);</script>'."\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($dashboardRowOpen)) { echo "</div>\n"; } ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript" src="/nice-select.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var selectize = document.querySelectorAll('select')
|
||||||
|
var options = {searchable: true};
|
||||||
|
selectize.forEach(function(select){
|
||||||
|
if( select.length > 30 && null === select.onchange ) {
|
||||||
|
select.classList.add("small", "selectize");
|
||||||
|
tabletd = select.closest('.field-row') || select.closest('td');
|
||||||
|
tabletd.style.cssText = 'overflow-x:unset';
|
||||||
|
NiceSelect.bind(select, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// Catalan ES Language Pack
|
||||||
|
// Rafa EA3BIL, Xavi EA3W
|
||||||
|
// 08-Mar-2018. Some updates done after linguistic expert review.
|
||||||
|
// 15-Oct-2018 . Ortographical fixes by C31FR - Franc
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Veu Digital",
|
||||||
|
"configuration" => "Configuració",
|
||||||
|
"dashboard_for" => "Tauler de control de",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Tauler de Control",
|
||||||
|
"admin" => "Administrar",
|
||||||
|
"power" => "Reiniciar/Apagar",
|
||||||
|
"update" => "Actualitzar",
|
||||||
|
"backup_restore" => "Fer/Restaurar còpia seguretat",
|
||||||
|
"factory_reset" => "Restaurar estat de fàbrica",
|
||||||
|
"live_logs" => "Informes/Logs",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Informació del maquinari",
|
||||||
|
"control_software" => "Control de programari",
|
||||||
|
"mmdvmhost_config" => "Configuració de MMDVMHost",
|
||||||
|
"general_config" => "Configuració General",
|
||||||
|
"dmr_config" => "Configuració de DMR",
|
||||||
|
"dstar_config" => "Configuració de DSTAR",
|
||||||
|
"ysf_config" => "Configuració de Yaesu C4FM fusion",
|
||||||
|
"p25_config" => "Configuració de P25",
|
||||||
|
"nxdn_config" => "Configuració de NXDN",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "Configuració de POCSAG",
|
||||||
|
"mobilegps_config" => "Configuració GPS mòbil",
|
||||||
|
"wifi_config" => "Configuració WIFI",
|
||||||
|
"fw_config" => "Configuració del Tallafocs",
|
||||||
|
"remote_access_pw" => "Mot clau per acccés remot",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Configuració",
|
||||||
|
"value" => "Valor",
|
||||||
|
"apply" => "Aplicar",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Nom de l'amfitrió",
|
||||||
|
"kernel" => "Nucli",
|
||||||
|
"platform" => "Plataforma",
|
||||||
|
"cpu_load" => "Càrrega CPU",
|
||||||
|
"cpu_temp" => "Temperatura CPU",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Controlador programari",
|
||||||
|
"controller_mode" => "Controlador de Mode",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "Mode DMR",
|
||||||
|
"d-star_mode" => "Mode D-Star",
|
||||||
|
"ysf_mode" => "Mode YSF",
|
||||||
|
"p25_mode" => "Mode P25",
|
||||||
|
"nxdn_mode" => "Mode NXDN",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM Tipus de pantalla",
|
||||||
|
"mode_hangtime" => "Mode temps de suspensió",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Indicatiu del Node",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "Freqüència RX/TX",
|
||||||
|
"lattitude" => "Latitud",
|
||||||
|
"longitude" => "Longitud",
|
||||||
|
"town" => "Ciutat",
|
||||||
|
"country" => "País",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Ràdio/Tipus de mòdem",
|
||||||
|
"node_type" => "Tipus de Node",
|
||||||
|
"timezone" => "Zona horària",
|
||||||
|
"dash_lang" => "Idioma del Tauler de Control",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "Màster DMR (MMDVMHost)",
|
||||||
|
"bm_master" => "Màster BrandMeister",
|
||||||
|
"bm_network" => "Xarxa BrandMeister",
|
||||||
|
"dmr_plus_master" => "Màster DMR+",
|
||||||
|
"dmr_plus_network" => "Xarxa DMR+",
|
||||||
|
"xlx_master" => "Màster XLX",
|
||||||
|
"xlx_enable" => "Habilitar màster XLX",
|
||||||
|
"dmr_cc" => "Codi de color DMR",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 indicatiu de crida",
|
||||||
|
"dstar_rpt2" => "RPT2 indicatiu de crida",
|
||||||
|
"dstar_irc_password" => "mot clau d'ircDDBGateway",
|
||||||
|
"dstar_default_ref" => "Reflector predeterminat",
|
||||||
|
"aprs_host" => "Servidor d'APRS",
|
||||||
|
"dstar_irc_lang" => "Idioma d'ircDDBGateway",
|
||||||
|
"dstar_irc_time" => "Interval de Balissa",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF Arrencant Host",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 Arrencant Host",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN Arrencant Host",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "Activació del GPS mòbil",
|
||||||
|
"mobilegps_port" => "Port del GPS",
|
||||||
|
"mobilegps_speed" => "Velocitat del port del GPS",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Tauler d'accés",
|
||||||
|
"fw_irc" => "ircDDBGateway Remot",
|
||||||
|
"fw_ssh" => "Accés per SSH",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "Nom d'usuari",
|
||||||
|
"password" => "contrasenya",
|
||||||
|
"set_password" => "Establir contrasenya",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Modes habilitats",
|
||||||
|
"net_status" => "Estat de la xarxa",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Informació de Ràdio",
|
||||||
|
"dstar_repeater" => "Repetidor D-Star",
|
||||||
|
"dstar_net" => "Xarxa D-Star",
|
||||||
|
"dmr_repeater" => "Repetidor DMR",
|
||||||
|
"dmr_master" => "Màster DMR",
|
||||||
|
"ysf_net" => "Xarxa YSF",
|
||||||
|
"p25_radio" => "Ràdio P25",
|
||||||
|
"p25_net" => "Xarxa P25",
|
||||||
|
"nxdn_radio" => "Ràdio NXDN",
|
||||||
|
"nxdn_net" => "Xarxa NXDN",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Hora",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Indicatiu",
|
||||||
|
"target" => "Destí",
|
||||||
|
"src" => "SRC",
|
||||||
|
"dur" => "DUR",
|
||||||
|
"loss" => "Pèrdua",
|
||||||
|
"ber" => "BER",
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "Activitat de passarel·la DAPNET",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Missatge",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Grup",
|
||||||
|
"logoff" => "Finalitzar",
|
||||||
|
"info" => "Informació",
|
||||||
|
"utot" => "UTOT",
|
||||||
|
"gtot" => "GTOT",
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Últimes 20 trucades rebudes",
|
||||||
|
"local_tx_list" => "Últimes 20 trucades rebudes via ràdio",
|
||||||
|
"active_starnet_groups" => "Grups actius Starnet",
|
||||||
|
"active_starnet_members" => "Membres actius grup Starnet",
|
||||||
|
"d-star_link_manager" => "Gestor d'enllaços D-Star",
|
||||||
|
"d-star_link_status" => "Informació d'enllaços D-Star",
|
||||||
|
"service_status" => "Estat del servei"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
// Chinese CN (simplified Chinese characters) Language Pack
|
||||||
|
// Translated by Le Peng(BD7KLE), Email: bd7kle@gmail.com
|
||||||
|
// Yuan Wang(BG3MDO), Email: bg3mdo@gmail.com
|
||||||
|
// Maintained by Yuan Wang(BG3MDO), Email: bg3mdo@gmail.com
|
||||||
|
// Last update on 03/08/2017
|
||||||
|
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "数字语音",
|
||||||
|
"configuration" => "配置",
|
||||||
|
"dashboard_for" => "仪表盘 -",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "仪表盘",
|
||||||
|
"admin" => "管理",
|
||||||
|
"power" => "电源",
|
||||||
|
"update" => "更新",
|
||||||
|
"backup_restore" => "备份/恢复",
|
||||||
|
"factory_reset" => "恢复出厂设置",
|
||||||
|
"live_logs" => "日志",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "网关硬件信息",
|
||||||
|
"control_software" => "控制软件",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost 配置",
|
||||||
|
"general_config" => "常规配置",
|
||||||
|
"dmr_config" => "DMR 配置",
|
||||||
|
"dstar_config" => "D-Star 配置",
|
||||||
|
"ysf_config" => "Yaesu System Fusion 配置",
|
||||||
|
"p25_config" => "P25 配置",
|
||||||
|
"nxdn_config" => "NXDN 配置",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG 配置",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"fw_config" => "防火墙配置",
|
||||||
|
"remote_access_pw" => "远程访问密码",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "设置",
|
||||||
|
"value" => "设置值",
|
||||||
|
"apply" => "应用设置",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "主机名",
|
||||||
|
"kernel" => "内核",
|
||||||
|
"platform" => "平台",
|
||||||
|
"cpu_load" => "CPU 负荷",
|
||||||
|
"cpu_temp" => "CPU 温度",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "控制器软件",
|
||||||
|
"controller_mode" => "控制器模式",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR 模式",
|
||||||
|
"d-star_mode" => "D-Star 模式",
|
||||||
|
"ysf_mode" => "YSF 模式",
|
||||||
|
"p25_mode" => "P25 模式",
|
||||||
|
"nxdn_mode" => "NXDN 模式",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM 显示屏类型",
|
||||||
|
"mode_hangtime" => "模式停留时间",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "节点呼号",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "电台频率",
|
||||||
|
"lattitude" => "纬度",
|
||||||
|
"longitude" => "经度",
|
||||||
|
"town" => "城市",
|
||||||
|
"country" => "国家",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "电台/调制解调器类型",
|
||||||
|
"node_type" => "节点类型",
|
||||||
|
"timezone" => "时区",
|
||||||
|
"dash_lang" => "仪表盘语言",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR 主机 (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister 主机 ",
|
||||||
|
"bm_network" => "BrandMeister 网络",
|
||||||
|
"dmr_plus_master" => "DMR+ 主机 ",
|
||||||
|
"dmr_plus_network" => "DMR+ 网络",
|
||||||
|
"xlx_master" => "XLX 主机 ",
|
||||||
|
"xlx_enable" => "XLX 主机启用",
|
||||||
|
"dmr_cc" => "DMR 彩色码",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 呼号",
|
||||||
|
"dstar_rpt2" => "RPT2 呼号",
|
||||||
|
"dstar_irc_password" => "ircDDBGateway 密码",
|
||||||
|
"dstar_default_ref" => "默认反射器",
|
||||||
|
"aprs_host" => "APRS 服务器",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway 语言",
|
||||||
|
"dstar_irc_time" => "时间通告",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF 默认服务器",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 默认服务器",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN 默认服务器",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "仪表盘访问",
|
||||||
|
"fw_irc" => "ircDDBGateway 远程",
|
||||||
|
"fw_ssh" => "SSH访问",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "用户名",
|
||||||
|
"password" => "密码",
|
||||||
|
"set_password" => "设置密码",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "启动的模式",
|
||||||
|
"net_status" => "网络状态",
|
||||||
|
"internet" => "互联网",
|
||||||
|
"radio_info" => "电台信息",
|
||||||
|
"dstar_repeater" => "D-Star 中继",
|
||||||
|
"dstar_net" => "D-Star 网络",
|
||||||
|
"dmr_repeater" => "DMR 中继",
|
||||||
|
"dmr_master" => "DMR 主机",
|
||||||
|
"ysf_net" => "YSF 网络",
|
||||||
|
"p25_radio" => "P25 电台",
|
||||||
|
"p25_net" => "P25 网络",
|
||||||
|
"nxdn_radio" => "NXDN 电台",
|
||||||
|
"nxdn_net" => "NXDN 网络",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "时间",
|
||||||
|
"mode" => "模式",
|
||||||
|
"callsign" => "呼号",
|
||||||
|
"target" => "目标",
|
||||||
|
"src" => "源", // Short version of "Source"
|
||||||
|
"dur" => "时长", // Short version of "Duration"
|
||||||
|
"loss" => "丢失",
|
||||||
|
"ber" => "误码率", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "组",
|
||||||
|
"logoff" => "注销",
|
||||||
|
"info" => "信息",
|
||||||
|
"utot" => "用户超时", // Short for User Timeout
|
||||||
|
"gtot" => "组超时", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "网关上最后 20 个呼叫",
|
||||||
|
"local_tx_list" => "最后 20 个本地呼叫",
|
||||||
|
"active_starnet_groups" => "激活的 Starnet 组",
|
||||||
|
"active_starnet_members" => "激活的 Starnet 组成员",
|
||||||
|
"d-star_link_manager" => "D-Star 连接管理器",
|
||||||
|
"d-star_link_status" => "D-Star 连接信息",
|
||||||
|
"service_status" => "服务状态"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
// Chinese HK (Traditional Chinese characters) Language Pack
|
||||||
|
// Translated by Le Peng(BD7KLE), Email: bd7kle@gmail.com
|
||||||
|
// Yuan Wang(BG3MDO), Email: bg3mdo@gmail.com
|
||||||
|
// Maintained by Yuan Wang(BG3MDO), Email: bg3mdo@gmail.com
|
||||||
|
// Last update on 03/08/2017
|
||||||
|
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "數字語音",
|
||||||
|
"configuration" => "設定",
|
||||||
|
"dashboard_for" => "儀錶盤 -",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "儀錶盤",
|
||||||
|
"admin" => "管理",
|
||||||
|
"power" => "電源",
|
||||||
|
"update" => "更新",
|
||||||
|
"backup_restore" => "備份/恢復",
|
||||||
|
"factory_reset" => "恢復出廠設定",
|
||||||
|
"live_logs" => "日誌",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "網關硬體信息",
|
||||||
|
"control_software" => "控制軟體",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost 設定",
|
||||||
|
"general_config" => "常規設定",
|
||||||
|
"dmr_config" => "DMR 設定",
|
||||||
|
"dstar_config" => "D-Star 設定",
|
||||||
|
"ysf_config" => "Yaesu System Fusion 設定",
|
||||||
|
"p25_config" => "P25 設定",
|
||||||
|
"nxdn_config" => "NXDN 設定",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG 設定",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"fw_config" => "防火墻設定",
|
||||||
|
"remote_access_pw" => "遠程訪問密碼",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "設定",
|
||||||
|
"value" => "設定值",
|
||||||
|
"apply" => "應用設定",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "主機名",
|
||||||
|
"kernel" => "內核",
|
||||||
|
"platform" => "平臺",
|
||||||
|
"cpu_load" => "CPU 負荷",
|
||||||
|
"cpu_temp" => "CPU 溫度",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "控制器軟體",
|
||||||
|
"controller_mode" => "控制器模式",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR 模式",
|
||||||
|
"d-star_mode" => "D-Star 模式",
|
||||||
|
"ysf_mode" => "YSF 模式",
|
||||||
|
"p25_mode" => "P25 模式",
|
||||||
|
"nxdn_mode" => "NXDN 模式",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM 顯示器類型",
|
||||||
|
"mode_hangtime" => "模式停留時間",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "節點呼號",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "電臺頻率",
|
||||||
|
"lattitude" => "緯度",
|
||||||
|
"longitude" => "經度",
|
||||||
|
"town" => "區域",
|
||||||
|
"country" => "國家",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "電臺/調製解調器類型",
|
||||||
|
"node_type" => "節點類型",
|
||||||
|
"timezone" => "時區",
|
||||||
|
"dash_lang" => "儀錶盤語言",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR 主機 (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister 主機 ",
|
||||||
|
"bm_network" => "BrandMeister 網絡",
|
||||||
|
"dmr_plus_master" => "DMR+ 主機 ",
|
||||||
|
"dmr_plus_network" => "DMR+ 網絡",
|
||||||
|
"xlx_master" => "XLX 主機 ",
|
||||||
|
"xlx_enable" => "XLX 主機啟用",
|
||||||
|
"dmr_cc" => "DMR 彩色碼",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 呼號",
|
||||||
|
"dstar_rpt2" => "RPT2 呼號",
|
||||||
|
"dstar_irc_password" => "ircDDBGateway 密碼",
|
||||||
|
"dstar_default_ref" => "默認反射器",
|
||||||
|
"aprs_host" => "APRS 服務器",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway 語言",
|
||||||
|
"dstar_irc_time" => "時間通告",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF 默認服務器",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 默認服務器",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN 默認服務器",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "儀表盤訪問",
|
||||||
|
"fw_irc" => "ircDDBGateway 遠程",
|
||||||
|
"fw_ssh" => "SSH訪問",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "用戶名",
|
||||||
|
"password" => "密碼",
|
||||||
|
"set_password" => "密碼設定",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "啟用的模式",
|
||||||
|
"net_status" => "網絡狀態",
|
||||||
|
"internet" => "互聯網",
|
||||||
|
"radio_info" => "電臺信息",
|
||||||
|
"dstar_repeater" => "D-Star 中繼",
|
||||||
|
"dstar_net" => "D-Star 網絡",
|
||||||
|
"dmr_repeater" => "DMR 中繼",
|
||||||
|
"dmr_master" => "DMR 主機",
|
||||||
|
"ysf_net" => "YSF 網絡",
|
||||||
|
"p25_radio" => "P25 電臺",
|
||||||
|
"p25_net" => "P25 網絡",
|
||||||
|
"nxdn_radio" => "NXDN 電臺",
|
||||||
|
"nxdn_net" => "NXDN 網絡",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "時間",
|
||||||
|
"mode" => "模式",
|
||||||
|
"callsign" => "呼號",
|
||||||
|
"target" => "目標",
|
||||||
|
"src" => "源", // Short version of "Source"
|
||||||
|
"dur" => "時長", // Short version of "Duration"
|
||||||
|
"loss" => "丟失",
|
||||||
|
"ber" => "誤碼率", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "組",
|
||||||
|
"logoff" => "註銷",
|
||||||
|
"info" => "信息",
|
||||||
|
"utot" => "用户超時", // Short for User Timeout
|
||||||
|
"gtot" => "組超時", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "網關上最後 20 個呼叫",
|
||||||
|
"local_tx_list" => "最後 20 個本地呼叫",
|
||||||
|
"active_starnet_groups" => "激活 Starnet 組",
|
||||||
|
"active_starnet_members" => "激活 Starnet 組成員",
|
||||||
|
"d-star_link_manager" => "D-Star 連接管理器",
|
||||||
|
"d-star_link_status" => "D-Star 連接信息",
|
||||||
|
"service_status" => "服務狀態"
|
||||||
|
);
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
// Chinese TW (Traditional Chinese characters) Language Pack
|
||||||
|
// Translated by Yngwie Chou(BU2EA), Email: bu2ea.tw@gmail.com
|
||||||
|
// Last update on 02/13/2018
|
||||||
|
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "數位語音",
|
||||||
|
"configuration" => "組態設定",
|
||||||
|
"dashboard_for" => "控制台 -",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "控制台",
|
||||||
|
"admin" => "管理",
|
||||||
|
"power" => "電源",
|
||||||
|
"update" => "更新",
|
||||||
|
"backup_restore" => "備份/還原",
|
||||||
|
"factory_reset" => "恢復出廠設定",
|
||||||
|
"live_logs" => "日誌",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "硬體資訊",
|
||||||
|
"control_software" => "控制軟體",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost 設定",
|
||||||
|
"general_config" => "一般設定",
|
||||||
|
"dmr_config" => "DMR 設定",
|
||||||
|
"dstar_config" => "D-Star 設定",
|
||||||
|
"ysf_config" => "Yaesu System Fusion 設定",
|
||||||
|
"p25_config" => "P25 設定",
|
||||||
|
"nxdn_config" => "NXDN 設定",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG 設定",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"fw_config" => "防火牆設定",
|
||||||
|
"remote_access_pw" => "遠端遙控密碼",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "設定",
|
||||||
|
"value" => "設定值",
|
||||||
|
"apply" => "變更設定",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "主機名稱",
|
||||||
|
"kernel" => "核心",
|
||||||
|
"platform" => "平台",
|
||||||
|
"cpu_load" => "CPU 負載",
|
||||||
|
"cpu_temp" => "CPU 溫度",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "控制器軟體",
|
||||||
|
"controller_mode" => "控制器模式",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR 模式",
|
||||||
|
"d-star_mode" => "D-Star 模式",
|
||||||
|
"ysf_mode" => "YSF 模式",
|
||||||
|
"p25_mode" => "P25 模式",
|
||||||
|
"nxdn_mode" => "NXDN 模式",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM 螢幕類型",
|
||||||
|
"mode_hangtime" => "模式保持時間",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "節點呼號",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "電台頻率",
|
||||||
|
"lattitude" => "緯度",
|
||||||
|
"longitude" => "經度",
|
||||||
|
"town" => "城市",
|
||||||
|
"country" => "國家",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Modem 類型",
|
||||||
|
"node_type" => "節點類型",
|
||||||
|
"timezone" => "時區",
|
||||||
|
"dash_lang" => "介面語言",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR 伺服器 (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister 伺服器",
|
||||||
|
"bm_network" => "BrandMeister 網路",
|
||||||
|
"dmr_plus_master" => "DMR+ 伺服器",
|
||||||
|
"dmr_plus_network" => "DMR+ 網路",
|
||||||
|
"xlx_master" => "XLX 伺服器",
|
||||||
|
"xlx_enable" => "啟用 XLX 伺服器",
|
||||||
|
"dmr_cc" => "DMR 色碼",
|
||||||
|
"dmr_embeddedlconly" => "僅允許指定 ID 連接",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 呼號",
|
||||||
|
"dstar_rpt2" => "RPT2 呼號",
|
||||||
|
"dstar_irc_password" => "ircDDBGateway 密碼",
|
||||||
|
"dstar_default_ref" => "預設反射器",
|
||||||
|
"aprs_host" => "APRS 伺服器",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway 語言",
|
||||||
|
"dstar_irc_time" => "時間提示",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF 預設伺服器",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 預設伺服器",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN 預設伺服器",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "控制台存取",
|
||||||
|
"fw_irc" => "ircDDBGateway 遠端遙控",
|
||||||
|
"fw_ssh" => "SSH 存取",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "用戶名",
|
||||||
|
"password" => "密碼",
|
||||||
|
"set_password" => "密碼設定",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "已啟用模式",
|
||||||
|
"net_status" => "網路狀態",
|
||||||
|
"internet" => "網際網路",
|
||||||
|
"radio_info" => "RF 電台資訊",
|
||||||
|
"dstar_repeater" => "D-Star 中繼",
|
||||||
|
"dstar_net" => "D-Star 網路",
|
||||||
|
"dmr_repeater" => "DMR 中繼",
|
||||||
|
"dmr_master" => "DMR 伺服器",
|
||||||
|
"ysf_net" => "YSF 網路",
|
||||||
|
"p25_radio" => "P25 電台",
|
||||||
|
"p25_net" => "P25 網路",
|
||||||
|
"nxdn_radio" => "NXDN 電台",
|
||||||
|
"nxdn_net" => "NXDN 網路",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "時間",
|
||||||
|
"mode" => "模式",
|
||||||
|
"callsign" => "呼號",
|
||||||
|
"target" => "目標",
|
||||||
|
"src" => "來源", // Short version of "Source"
|
||||||
|
"dur" => "歷時", // Short version of "Duration"
|
||||||
|
"loss" => "丟失率",
|
||||||
|
"ber" => "誤碼率", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "群組",
|
||||||
|
"logoff" => "登出",
|
||||||
|
"info" => "資訊",
|
||||||
|
"utot" => "用户逾時", // Short for User Timeout
|
||||||
|
"gtot" => "群組逾時", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "最近 20 個通訊",
|
||||||
|
"local_tx_list" => "最近 20 個本地 RF 通訊",
|
||||||
|
"active_starnet_groups" => "啟用 Starnet 群組",
|
||||||
|
"active_starnet_members" => "啟用 Starnet 成員",
|
||||||
|
"d-star_link_manager" => "D-Star 連線管理",
|
||||||
|
"d-star_link_status" => "D-Star 連線狀態",
|
||||||
|
"service_status" => "服務狀態"
|
||||||
|
);
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// Dutch NL Language Pack for Pi-Star translated by Rob van Rheenen P0DIB
|
||||||
|
// Updated: 04-Aug-2017
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Digital Voice",
|
||||||
|
"configuration" => "Configuratie",
|
||||||
|
"dashboard_for" => "Dashboard voor",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Dashboard",
|
||||||
|
"admin" => "Admin",
|
||||||
|
"power" => "Power",
|
||||||
|
"update" => "Update",
|
||||||
|
"backup_restore" => "Backup/Restore",
|
||||||
|
"factory_reset" => "Reset all",
|
||||||
|
"live_logs" => "Actuele Logs",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Gateway hardware informatie",
|
||||||
|
"control_software" => "Controller Host software",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost configuratie",
|
||||||
|
"general_config" => "Algemene configuratie",
|
||||||
|
"dmr_config" => "DMR configuratie",
|
||||||
|
"dstar_config" => "D-Star configuratie",
|
||||||
|
"ysf_config" => "Yaesu System Fusion configuratie",
|
||||||
|
"p25_config" => "P25 configuratie",
|
||||||
|
"nxdn_config" => "NXDN configuratie",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG configuratie",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"wifi_config" => "Wireless configuratie",
|
||||||
|
"fw_config" => "Firewall configuratie",
|
||||||
|
"remote_access_pw" => "Remote Access paswoord",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Instelling",
|
||||||
|
"value" => "Waarde",
|
||||||
|
"apply" => "Toepassen",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Hostnaam",
|
||||||
|
"kernel" => "Kernel",
|
||||||
|
"platform" => "Platform",
|
||||||
|
"cpu_load" => "CPU Load",
|
||||||
|
"cpu_temp" => "CPU Temp",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Controller software",
|
||||||
|
"controller_mode" => "Controller modus",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR modus",
|
||||||
|
"d-star_mode" => "D-Star modus",
|
||||||
|
"ysf_mode" => "YSF modus",
|
||||||
|
"p25_mode" => "P25 modus",
|
||||||
|
"nxdn_mode" => "NXDN modus",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM display type",
|
||||||
|
"mode_hangtime" => "Mode hangtijd",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Node roepletters",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "Radio frequentie",
|
||||||
|
"lattitude" => "Breedtegraad",
|
||||||
|
"longitude" => "Lengtegraad",
|
||||||
|
"town" => "Plaats",
|
||||||
|
"country" => "Land",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Radio/Modem type",
|
||||||
|
"node_type" => "Node type",
|
||||||
|
"timezone" => "Systeem tijdzone",
|
||||||
|
"dash_lang" => "Dashboard taal",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR Master (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister Master",
|
||||||
|
"bm_network" => "BrandMeister netwerk",
|
||||||
|
"dmr_plus_master" => "DMR+ Master",
|
||||||
|
"dmr_plus_network" => "DMR+ netwerk",
|
||||||
|
"xlx_master" => "XLX Master",
|
||||||
|
"xlx_enable" => "XLX Master AAN",
|
||||||
|
"dmr_cc" => "DMR kleur code (CC)",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 roepletters",
|
||||||
|
"dstar_rpt2" => "RPT2 roepletters",
|
||||||
|
"dstar_irc_password" => "ircDDBGateway paswoord",
|
||||||
|
"dstar_default_ref" => "Standaard reflector",
|
||||||
|
"aprs_host" => "APRS Host",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway taal",
|
||||||
|
"dstar_irc_time" => "Tijd uitzenden",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF standaard Host",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 standaard Host",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN standaard Host",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Dashboard toegang",
|
||||||
|
"fw_irc" => "ircDDBGateway op afstand",
|
||||||
|
"fw_ssh" => "SSH toegang",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "Gebruikersnaam",
|
||||||
|
"password" => "Paswoord",
|
||||||
|
"set_password" => "Voer paswoord in",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Actieve modi",
|
||||||
|
"net_status" => "Netwerk status",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Radio info",
|
||||||
|
"dstar_repeater" => "D-Star repeater",
|
||||||
|
"dstar_net" => "D-Star netwerk",
|
||||||
|
"dmr_repeater" => "DMR repeater",
|
||||||
|
"dmr_master" => "DMR Master",
|
||||||
|
"ysf_net" => "YSF netwerk",
|
||||||
|
"p25_radio" => "P25 Radio",
|
||||||
|
"p25_net" => "P25 netwerk",
|
||||||
|
"nxdn_radio" => "NXDN Radio",
|
||||||
|
"nxdn_net" => "NXDN netwerk",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Tijd",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Roepletters",
|
||||||
|
"target" => "Doel",
|
||||||
|
"src" => "Bron", // Short version of "Source"
|
||||||
|
"dur" => "Duur", // Short version of "Duration"
|
||||||
|
"loss" => "Verlies",
|
||||||
|
"ber" => "BER", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Groep",
|
||||||
|
"logoff" => "Uitloggen",
|
||||||
|
"info" => "Informatie",
|
||||||
|
"utot" => "UTOT", // Short for User Timeout
|
||||||
|
"gtot" => "GTOT", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Laatste 20 oproepen gehoord (RX) via deze Gateway",
|
||||||
|
"local_tx_list" => "Laatste 20 oproepen verzonden (TX) via deze Gateway",
|
||||||
|
"active_starnet_groups" => "Actieve Starnet groepen",
|
||||||
|
"active_starnet_members" => "Actieve Starnet groepsleden",
|
||||||
|
"d-star_link_manager" => "D-Star Link Manager",
|
||||||
|
"d-star_link_status" => "D-Star Link Informatie",
|
||||||
|
"service_status" => "Service Status"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// English UK Language Pack
|
||||||
|
// Andy Taylor (MW0MWZ)
|
||||||
|
// Updated: 02-Aug-2017
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Digital Voice",
|
||||||
|
"configuration" => "Configuration",
|
||||||
|
"dashboard_for" => "Dashboard for",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Dashboard",
|
||||||
|
"admin" => "Admin",
|
||||||
|
"power" => "Power",
|
||||||
|
"update" => "Update",
|
||||||
|
"backup_restore" => "Backup/Restore",
|
||||||
|
"factory_reset" => "Factory Reset",
|
||||||
|
"live_logs" => "Live Logs",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Gateway Hardware Information",
|
||||||
|
"control_software" => "Control Software",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost Configuration",
|
||||||
|
"general_config" => "General Configuration",
|
||||||
|
"dmr_config" => "DMR Configuration",
|
||||||
|
"dstar_config" => "D-Star Configuration",
|
||||||
|
"ysf_config" => "Yaesu System Fusion Configuration",
|
||||||
|
"p25_config" => "P25 Configuration",
|
||||||
|
"nxdn_config" => "NXDN Configuration",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG Configuration",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"wifi_config" => "Wireless Configuration",
|
||||||
|
"fw_config" => "Firewall Configuration",
|
||||||
|
"remote_access_pw" => "Remote Access Password",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Setting",
|
||||||
|
"value" => "Value",
|
||||||
|
"apply" => "Apply Changes",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Hostname",
|
||||||
|
"kernel" => "Kernel",
|
||||||
|
"platform" => "Platform",
|
||||||
|
"cpu_load" => "CPU Load",
|
||||||
|
"cpu_temp" => "CPU Temp",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Controller Software",
|
||||||
|
"controller_mode" => "Controller Mode",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR Mode",
|
||||||
|
"d-star_mode" => "D-Star Mode",
|
||||||
|
"ysf_mode" => "YSF Mode",
|
||||||
|
"p25_mode" => "P25 Mode",
|
||||||
|
"nxdn_mode" => "NXDN Mode",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM Display Type",
|
||||||
|
"mode_hangtime" => "Mode Hangtime",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Node Callsign",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "Radio Frequency",
|
||||||
|
"lattitude" => "Latitude",
|
||||||
|
"longitude" => "Longitude",
|
||||||
|
"town" => "Town",
|
||||||
|
"country" => "Country",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Radio/Modem Type",
|
||||||
|
"node_type" => "Node Type",
|
||||||
|
"timezone" => "System Time Zone",
|
||||||
|
"dash_lang" => "Dashboard Language",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR Master (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister Master",
|
||||||
|
"bm_network" => "BrandMeister Network",
|
||||||
|
"dmr_plus_master" => "DMR+ Master",
|
||||||
|
"dmr_plus_network" => "DMR+ Network",
|
||||||
|
"xlx_master" => "XLX Master",
|
||||||
|
"xlx_enable" => "XLX Master Enable",
|
||||||
|
"dmr_cc" => "DMR Colour Code",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 Callsign",
|
||||||
|
"dstar_rpt2" => "RPT2 Callsign",
|
||||||
|
"dstar_irc_password" => "Remote Password",
|
||||||
|
"dstar_default_ref" => "Default Reflector",
|
||||||
|
"aprs_host" => "APRS Host",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway Language",
|
||||||
|
"dstar_irc_time" => "Time Announcements",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF Startup Host",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 Startup Host",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN Startup Host",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Dashboard Access",
|
||||||
|
"fw_irc" => "ircDDBGateway Remote",
|
||||||
|
"fw_ssh" => "SSH Access",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "User Name",
|
||||||
|
"password" => "Password",
|
||||||
|
"set_password" => "Set Password",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Modes Enabled",
|
||||||
|
"net_status" => "Network Status",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Radio Info",
|
||||||
|
"dstar_repeater" => "D-Star Repeater",
|
||||||
|
"dstar_net" => "D-Star Network",
|
||||||
|
"dmr_repeater" => "DMR Repeater",
|
||||||
|
"dmr_master" => "DMR Master",
|
||||||
|
"ysf_net" => "YSF Network",
|
||||||
|
"p25_radio" => "P25 Radio",
|
||||||
|
"p25_net" => "P25 Network",
|
||||||
|
"nxdn_radio" => "NXDN Radio",
|
||||||
|
"nxdn_net" => "NXDN Network",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Time",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Callsign",
|
||||||
|
"target" => "Target",
|
||||||
|
"src" => "Src", // Short version of "Source"
|
||||||
|
"dur" => "Dur", // Short version of "Duration"
|
||||||
|
"loss" => "Loss",
|
||||||
|
"ber" => "BER", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Group",
|
||||||
|
"logoff" => "LogOff",
|
||||||
|
"info" => "Information",
|
||||||
|
"utot" => "UTOT", // Short for User Timeout
|
||||||
|
"gtot" => "GTOT", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Gateway Activity",
|
||||||
|
"local_tx_list" => "Local RF Activity",
|
||||||
|
"active_starnet_groups" => "Active Starnet Groups",
|
||||||
|
"active_starnet_members" => "Active Starnet Group Members",
|
||||||
|
"d-star_link_manager" => "D-Star Link Manager",
|
||||||
|
"d-star_link_status" => "D-Star Link Information",
|
||||||
|
"service_status" => "Service Status"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// English US Language Pack
|
||||||
|
// Andy Taylor (MW0MWZ)
|
||||||
|
// Updated: 02-Aug-2017
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Digital Voice",
|
||||||
|
"configuration" => "Configuration",
|
||||||
|
"dashboard_for" => "Dashboard for",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Dashboard",
|
||||||
|
"admin" => "Admin",
|
||||||
|
"power" => "Power",
|
||||||
|
"update" => "Update",
|
||||||
|
"backup_restore" => "Backup/Restore",
|
||||||
|
"factory_reset" => "Factory Reset",
|
||||||
|
"live_logs" => "Live Logs",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Gateway Hardware Information",
|
||||||
|
"control_software" => "Control Software",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost Configuration",
|
||||||
|
"general_config" => "General Configuration",
|
||||||
|
"dmr_config" => "DMR Configuration",
|
||||||
|
"dstar_config" => "D-Star Configuration",
|
||||||
|
"ysf_config" => "Yaesu System Fusion Configuration",
|
||||||
|
"p25_config" => "P25 Configuration",
|
||||||
|
"nxdn_config" => "NXDN Configuration",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG Configuration",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"wifi_config" => "Wireless Configuration",
|
||||||
|
"fw_config" => "Firewall Configuration",
|
||||||
|
"remote_access_pw" => "Remote Access Password",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Setting",
|
||||||
|
"value" => "Value",
|
||||||
|
"apply" => "Apply Changes",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Hostname",
|
||||||
|
"kernel" => "Kernel",
|
||||||
|
"platform" => "Platform",
|
||||||
|
"cpu_load" => "CPU Load",
|
||||||
|
"cpu_temp" => "CPU Temp",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Controller Software",
|
||||||
|
"controller_mode" => "Controller Mode",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR Mode",
|
||||||
|
"d-star_mode" => "D-Star Mode",
|
||||||
|
"ysf_mode" => "YSF Mode",
|
||||||
|
"p25_mode" => "P25 Mode",
|
||||||
|
"nxdn_mode" => "NXDN Mode",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM Display Type",
|
||||||
|
"mode_hangtime" => "Mode Hangtime",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Node Callsign",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "Radio Frequency",
|
||||||
|
"lattitude" => "Latitude",
|
||||||
|
"longitude" => "Longitude",
|
||||||
|
"town" => "Town",
|
||||||
|
"country" => "Country",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Radio/Modem Type",
|
||||||
|
"node_type" => "Node Type",
|
||||||
|
"timezone" => "System Time Zone",
|
||||||
|
"dash_lang" => "Dashboard Language",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR Master (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister Master",
|
||||||
|
"bm_network" => "BrandMeister Network",
|
||||||
|
"dmr_plus_master" => "DMR+ Master",
|
||||||
|
"dmr_plus_network" => "DMR+ Network",
|
||||||
|
"xlx_master" => "XLX Master",
|
||||||
|
"xlx_enable" => "XLX Master Enable",
|
||||||
|
"dmr_cc" => "DMR Color Code",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 Callsign",
|
||||||
|
"dstar_rpt2" => "RPT2 Callsign",
|
||||||
|
"dstar_irc_password" => "Remote Password",
|
||||||
|
"dstar_default_ref" => "Default Reflector",
|
||||||
|
"aprs_host" => "APRS Host",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway Language",
|
||||||
|
"dstar_irc_time" => "Time Announcements",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF Startup Host",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 Startup Host",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN Startup Host",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Dashboard Access",
|
||||||
|
"fw_irc" => "ircDDBGateway Remote",
|
||||||
|
"fw_ssh" => "SSH Access",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "User Name",
|
||||||
|
"password" => "Password",
|
||||||
|
"set_password" => "Set Password",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Modes Enabled",
|
||||||
|
"net_status" => "Network Status",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Radio Info",
|
||||||
|
"dstar_repeater" => "D-Star Repeater",
|
||||||
|
"dstar_net" => "D-Star Network",
|
||||||
|
"dmr_repeater" => "DMR Repeater",
|
||||||
|
"dmr_master" => "DMR Master",
|
||||||
|
"ysf_net" => "YSF Network",
|
||||||
|
"p25_radio" => "P25 Radio",
|
||||||
|
"p25_net" => "P25 Network",
|
||||||
|
"nxdn_radio" => "NXDN Radio",
|
||||||
|
"nxdn_net" => "NXDN Network",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Time",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Callsign",
|
||||||
|
"target" => "Target",
|
||||||
|
"src" => "Src", // Short version of "Source"
|
||||||
|
"dur" => "Dur", // Short version of "Duration"
|
||||||
|
"loss" => "Loss",
|
||||||
|
"ber" => "BER", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Group",
|
||||||
|
"logoff" => "LogOff",
|
||||||
|
"info" => "Information",
|
||||||
|
"utot" => "UTOT", // Short for User Timeout
|
||||||
|
"gtot" => "GTOT", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Gateway Activity",
|
||||||
|
"local_tx_list" => "Local RF Activity",
|
||||||
|
"active_starnet_groups" => "Active Starnet Groups",
|
||||||
|
"active_starnet_members" => "Active Starnet Group Members",
|
||||||
|
"d-star_link_manager" => "D-Star Link Manager",
|
||||||
|
"d-star_link_status" => "D-Star Link Information",
|
||||||
|
"service_status" => "Service Status"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// Frehcn FR Language Pack
|
||||||
|
// Pierre Philippe (F4MZI)
|
||||||
|
// Updated: 01-Nov-2017
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Relais numérique",
|
||||||
|
"configuration" => "Configuration",
|
||||||
|
"dashboard_for" => "Console pour",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Console",
|
||||||
|
"admin" => "Administration",
|
||||||
|
"power" => "Arrêt/Redémarrage",
|
||||||
|
"update" => "Mise à jour",
|
||||||
|
"backup_restore" => "Sauvegarde/Restauration",
|
||||||
|
"factory_reset" => "Réinitialisation Usine",
|
||||||
|
"live_logs" => "Surveillance des Logs",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Informations matérielles de la passerelle",
|
||||||
|
"control_software" => "Contrôle logiciel",
|
||||||
|
"mmdvmhost_config" => "Configuration de MMDVMHost",
|
||||||
|
"general_config" => "Configuration générale",
|
||||||
|
"dmr_config" => "Configuration DMR",
|
||||||
|
"dstar_config" => "Configuration D-STAR",
|
||||||
|
"ysf_config" => "Configuration Yaesu System Fusion",
|
||||||
|
"p25_config" => "Configuration P25",
|
||||||
|
"nxdn_config" => "Configuration NXDN",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "Configuration POCSAG",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"wifi_config" => "Configuration Réseau WIFI",
|
||||||
|
"fw_config" => "Configuration du Firewall",
|
||||||
|
"remote_access_pw" => "Mot de passe accès distant",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Paramètres",
|
||||||
|
"value" => "Valeur",
|
||||||
|
"apply" => "Appliquer les modifications",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Nom d'hôte",
|
||||||
|
"kernel" => "Kernel",
|
||||||
|
"platform" => "Plateforme",
|
||||||
|
"cpu_load" => "Charge CPU",
|
||||||
|
"cpu_temp" => "Température CPU",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Logiciel controleur",
|
||||||
|
"controller_mode" => "Mode controleur",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "Mode DMR",
|
||||||
|
"d-star_mode" => "Mode D-Star",
|
||||||
|
"ysf_mode" => "Mode YSF",
|
||||||
|
"p25_mode" => "Mode P25",
|
||||||
|
"nxdn_mode" => "Mode NXDN",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "Afficheur MMDVM",
|
||||||
|
"mode_hangtime" => "Mode Hangtime",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Indicatif du Node",
|
||||||
|
"dmr_id" => "Id CCS7/DMR",
|
||||||
|
"radio_freq" => "Fréquence radio",
|
||||||
|
"lattitude" => "Latitude",
|
||||||
|
"longitude" => "Longitude",
|
||||||
|
"town" => "Ville",
|
||||||
|
"country" => "Pays",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Modèle Radio/Modem",
|
||||||
|
"node_type" => "Type de Node",
|
||||||
|
"timezone" => "Fuseau horaire",
|
||||||
|
"dash_lang" => "Langage de la console",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "Master DMR(MMDVMHost)",
|
||||||
|
"bm_master" => "Master BrandMeister",
|
||||||
|
"bm_network" => "Réseau BrandMeister",
|
||||||
|
"dmr_plus_master" => "Master DMR+",
|
||||||
|
"dmr_plus_network" => "Réseau DMR+",
|
||||||
|
"xlx_master" => "Master XLX",
|
||||||
|
"xlx_enable" => "Master XLX actif",
|
||||||
|
"dmr_cc" => "Code Couleur DMR",
|
||||||
|
"dmr_embeddedlconly" => "DMR LC intégré uniquement",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "Indicatif RPT1",
|
||||||
|
"dstar_rpt2" => "Indicatif RPT2",
|
||||||
|
"dstar_irc_password" => "Mot de passe ircDDBGateway",
|
||||||
|
"dstar_default_ref" => "Reflecteur par défaut",
|
||||||
|
"aprs_host" => "Hôte APRS",
|
||||||
|
"dstar_irc_lang" => "Langage ircDDBGateway",
|
||||||
|
"dstar_irc_time" => "Annonces horaires",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "Room YSF au démarrage",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "Hôte de démarrage P25",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "Hôte de démarrage NXDN",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Accès Console",
|
||||||
|
"fw_irc" => "Commande à distance ircDDBGateway",
|
||||||
|
"fw_ssh" => "Accès SSH",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "Nom d'utilisateur",
|
||||||
|
"password" => "Mot de passe",
|
||||||
|
"set_password" => "Définir le mot de passe",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Modes actifs",
|
||||||
|
"net_status" => "État du réseau",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Info Radio",
|
||||||
|
"dstar_repeater" => "Relais D-Star",
|
||||||
|
"dstar_net" => "Réseau D-Star",
|
||||||
|
"dmr_repeater" => "Relais DMR",
|
||||||
|
"dmr_master" => "Master DMR",
|
||||||
|
"ysf_net" => "Réseau YSF",
|
||||||
|
"p25_radio" => "Radio P25",
|
||||||
|
"p25_net" => "Réseau P25",
|
||||||
|
"nxdn_radio" => "Radio NXDN",
|
||||||
|
"nxdn_net" => "Réseau NXDN",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Heure",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Indicatif ",
|
||||||
|
"target" => "Cible",
|
||||||
|
"src" => "Source ", // Short version of "Source"
|
||||||
|
"dur" => "Durée ", // Short version of "Duration"
|
||||||
|
"loss" => "Pertes",
|
||||||
|
"ber" => "BER", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Groupe",
|
||||||
|
"logoff" => "Déconnexion",
|
||||||
|
"info" => "Information",
|
||||||
|
"utot" => "UTOT", // Short for User Timeout
|
||||||
|
"gtot" => "GTOT", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Activité de la passerelle",
|
||||||
|
"local_tx_list" => "Activité locale de la voie radio",
|
||||||
|
"active_starnet_groups" => "Active Starnet Groups",
|
||||||
|
"active_starnet_members" => "Active Starnet Group Members",
|
||||||
|
"d-star_link_manager" => "Administrateur du lien D-Star",
|
||||||
|
"d-star_link_status" => "Informations du lien D-Star",
|
||||||
|
"service_status" => "État du service"
|
||||||
|
);
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
//
|
||||||
|
// German Language Pack
|
||||||
|
// by Klaus, DL5RFK
|
||||||
|
// 08-Aug-2017
|
||||||
|
//
|
||||||
|
$lang = array (
|
||||||
|
// Banner texts
|
||||||
|
"digital_voice" => "Digital Voice",
|
||||||
|
"configuration" => "Konfiguration",
|
||||||
|
"dashboard_for" => "Tableau für",
|
||||||
|
// Banner links
|
||||||
|
"dashboard" => "Tableau",
|
||||||
|
"admin" => "Admin",
|
||||||
|
"power" => "Strom",
|
||||||
|
"update" => "Aktualisieren",
|
||||||
|
"backup_restore" => "Datensicherung/Wiederherstellung",
|
||||||
|
"factory_reset" => "Werkseinstellung",
|
||||||
|
"live_logs" => "Protokoll",
|
||||||
|
// Config page section headdings
|
||||||
|
"hardware_info" => "Gateway Hardware Information",
|
||||||
|
"control_software" => "Kontrollsoftware",
|
||||||
|
"mmdvmhost_config" => "MMDVMHost Konfiguration",
|
||||||
|
"general_config" => "Basis Konfiguration",
|
||||||
|
"dmr_config" => "DMR Konfiguration",
|
||||||
|
"dstar_config" => "D-Star Konfiguration",
|
||||||
|
"ysf_config" => "Yaesu System Fusion Konfiguration",
|
||||||
|
"p25_config" => "P25 Konfiguration",
|
||||||
|
"nxdn_config" => "NXDN Konfiguration",
|
||||||
|
"m17_config" => "M17 Configuration",
|
||||||
|
"pocsag_config" => "POCSAG Konfiguration",
|
||||||
|
"mobilegps_config" => "Mobile GPS Configuration",
|
||||||
|
"wifi_config" => "Wlan Konfiguration",
|
||||||
|
"fw_config" => "Firewall Konfiguration",
|
||||||
|
"remote_access_pw" => "Fernzugriff",
|
||||||
|
// Config Page - Section General
|
||||||
|
"setting" => "Einstellung",
|
||||||
|
"value" => "Wert",
|
||||||
|
"apply" => "Speichern",
|
||||||
|
// Config Page - Gateway Hardware Information
|
||||||
|
"hostname" => "Rechnername",
|
||||||
|
"kernel" => "Kernel",
|
||||||
|
"platform" => "Plattform",
|
||||||
|
"cpu_load" => "CPU Last",
|
||||||
|
"cpu_temp" => "CPU Temp",
|
||||||
|
// Config Page - Control Software
|
||||||
|
"controller_software" => "Kontroller Software",
|
||||||
|
"controller_mode" => "Kontroller Mode",
|
||||||
|
// Config Page - MMDVMHost Configuration
|
||||||
|
"dmr_mode" => "DMR Modus",
|
||||||
|
"d-star_mode" => "D-Star Modus",
|
||||||
|
"ysf_mode" => "YSF Modus",
|
||||||
|
"p25_mode" => "P25 Modus",
|
||||||
|
"nxdn_mode" => "NXDN Modus",
|
||||||
|
"m17_mode" => "M17 Mode",
|
||||||
|
"mmdvm_display" => "MMDVM Display Typ",
|
||||||
|
"mode_hangtime" => "Mode Nachlaufzeit",
|
||||||
|
// Config Page - General Configuration
|
||||||
|
"node_call" => "Node Rufzeichen",
|
||||||
|
"dmr_id" => "CCS7/DMR ID",
|
||||||
|
"radio_freq" => "Radio Frequenz",
|
||||||
|
"lattitude" => "Breitengrad",
|
||||||
|
"longitude" => "Längengrad",
|
||||||
|
"town" => "Stadt",
|
||||||
|
"country" => "Land",
|
||||||
|
"url" => "URL",
|
||||||
|
"radio_type" => "Radio/Modem Typ",
|
||||||
|
"node_type" => "Node Typ",
|
||||||
|
"timezone" => "Systemzeit Zone",
|
||||||
|
"dash_lang" => "Tableau Sprache",
|
||||||
|
// Config Page - DMR Configuration
|
||||||
|
"dmr_master" => "DMR Master (MMDVMHost)",
|
||||||
|
"bm_master" => "BrandMeister Master",
|
||||||
|
"bm_network" => "BrandMeister Netzwerk",
|
||||||
|
"dmr_plus_master" => "DMR+ Master",
|
||||||
|
"dmr_plus_network" => "DMR+ Netzwerk",
|
||||||
|
"xlx_master" => "XLX Master",
|
||||||
|
"xlx_enable" => "XLX Master Aktiv",
|
||||||
|
"dmr_cc" => "DMR Color Code",
|
||||||
|
"dmr_embeddedlconly" => "DMR EmbeddedLCOnly",
|
||||||
|
"dmr_dumptadata" => "DMR DumpTAData",
|
||||||
|
// Config Page - D-Star Configuration
|
||||||
|
"dstar_rpt1" => "RPT1 Rufzeichen",
|
||||||
|
"dstar_rpt2" => "RPT2 Rufzeichen",
|
||||||
|
"dstar_irc_password" => "ircDDBGateway Passwort",
|
||||||
|
"dstar_default_ref" => "Standard Reflektor",
|
||||||
|
"aprs_host" => "APRS Host",
|
||||||
|
"dstar_irc_lang" => "ircDDBGateway Sprache",
|
||||||
|
"dstar_irc_time" => "Zeit Ansagen",
|
||||||
|
// Config Page - YSF Configuration
|
||||||
|
"ysf_startup_host" => "YSF Startup Host",
|
||||||
|
// Config Page - P25 Configuration
|
||||||
|
"p25_startup_host" => "P25 Startup Host",
|
||||||
|
"p25_nac" => "P25 NAC",
|
||||||
|
// Config Page - NXDN Configuration
|
||||||
|
"nxdn_startup_host" => "NXDN Startup Host",
|
||||||
|
"nxdn_ran" => "NXDN RAN",
|
||||||
|
// Config Page - M17 Configuration
|
||||||
|
"m17_startup_host" => "M17 Startup Host",
|
||||||
|
"m17_can" => "M17 CAN",
|
||||||
|
// Config Page - MobileGPS Configuration
|
||||||
|
"mobilegps_enable" => "MobileGPS Enable",
|
||||||
|
"mobilegps_port" => "GPS Port",
|
||||||
|
"mobilegps_speed" => "GPS Port Speed",
|
||||||
|
// Config Page - Firewall Configuration
|
||||||
|
"fw_dash" => "Tableau Zugriff",
|
||||||
|
"fw_irc" => "ircDDBGateway Remote",
|
||||||
|
"fw_ssh" => "SSH Zugriff",
|
||||||
|
// Config Page - Password
|
||||||
|
"user" => "Benutzername",
|
||||||
|
"password" => "Passwort",
|
||||||
|
"set_password" => "Passwort setzen",
|
||||||
|
// Dashboard Front Page - Repeater Info Pannel
|
||||||
|
"modes_enabled" => "Aktive Modi",
|
||||||
|
"net_status" => "Netzwerk Status",
|
||||||
|
"internet" => "Internet",
|
||||||
|
"radio_info" => "Radio Info",
|
||||||
|
"dstar_repeater" => "D-Star Relais",
|
||||||
|
"dstar_net" => "D-Star Netzwerk",
|
||||||
|
"dmr_repeater" => "DMR Relais",
|
||||||
|
"dmr_master" => "DMR Master",
|
||||||
|
"ysf_net" => "YSF Netzwerk",
|
||||||
|
"p25_radio" => "P25 Radio",
|
||||||
|
"p25_net" => "P25 Netzwerk",
|
||||||
|
"nxdn_radio" => "NXDN Radio",
|
||||||
|
"nxdn_net" => "NXDN Netzwerk",
|
||||||
|
"m17_radio" => "M17 Radio",
|
||||||
|
"m17_net" => "M17 Network",
|
||||||
|
// Dashboard Front Page - Calls
|
||||||
|
"time" => "Zeit",
|
||||||
|
"mode" => "Mode",
|
||||||
|
"callsign" => "Rufzeichen",
|
||||||
|
"target" => "Ziel",
|
||||||
|
"src" => "Quelle", // Short version of "Source"
|
||||||
|
"dur" => "Dauer", // Short version of "Duration"
|
||||||
|
"loss" => "Verlust",
|
||||||
|
"ber" => "BER", // Short version of "Bit Error Rate"
|
||||||
|
// POCSAG Specific
|
||||||
|
"pocsag_list" => "DAPNET Gateway Activity",
|
||||||
|
"pocsag_timeslot" => "Time Slot",
|
||||||
|
"pocsag_msg" => "Message",
|
||||||
|
// Dashboard - Extra Info
|
||||||
|
"group" => "Gruppe",
|
||||||
|
"logoff" => "LogOff",
|
||||||
|
"info" => "Information",
|
||||||
|
"utot" => "UTOT", // Short for User Timeout
|
||||||
|
"gtot" => "GTOT", // Short for Group Timeout
|
||||||
|
// Dashboard Front Page / Admin - Section Headders
|
||||||
|
"last_heard_list" => "Letzten 20 Rufzeichen, die gehört wurden",
|
||||||
|
"local_tx_list" => "Letzten 20 Rufzeichen, die dieses Gateway nutzten",
|
||||||
|
"active_starnet_groups" => "Aktive Starnet Gruppen",
|
||||||
|
"active_starnet_members" => "Aktive Starnet Gruppen Mitglieder",
|
||||||
|
"d-star_link_manager" => "D-Star Link Manager",
|
||||||
|
"d-star_link_status" => "D-Star Link Information",
|
||||||
|
"service_status" => "Service Status"
|
||||||
|
);
|
||||||