├── AllScan-logo.png ├── AllScan.png ├── AllScanInstallUpdate.php ├── LICENSE ├── README.md ├── _tools ├── agi-bin │ ├── getip.sh │ ├── poweroff.sh │ ├── poweroff.ul │ ├── reboot.sh │ ├── reboot.ul │ ├── sayip.sh │ ├── test.ul │ ├── wifidisabled.ul │ ├── wifidown.sh │ ├── wifienabled.ul │ └── wifiup.sh ├── cgrep.php ├── checkFs.php ├── copyFromWww.php ├── copyToWww.php ├── diffWww.php ├── excludeFiles.txt ├── setUpDtmfCmds.php └── wgrep.php ├── api └── index.php ├── astapi ├── AMI.php ├── cmd.php ├── connect.php ├── nodeInfo.php └── server.php ├── cfg ├── CfgView.php └── index.php ├── css └── main.css ├── docs ├── extensions.conf ├── favorites.ini.sample ├── global.inc.sample ├── rpt.conf └── screenshots │ ├── cfgs.png │ ├── init.png │ ├── settings.png │ ├── user-edit.png │ └── users.png ├── favicon.ico ├── favorites-Sample.ini ├── include ├── CfgModel.php ├── DB.php ├── DataSource.php ├── Html.php ├── UserModel.php ├── apiInit.php ├── common.php ├── commonForms.php ├── dbUtils.php ├── favsUtils.php ├── hwUtils.php ├── logUtils.php ├── timeUtils.php ├── timezones.php ├── version.txt └── viewUtils.php ├── index.php ├── js └── main.js ├── screenshot.png ├── stats └── stats.php └── user ├── UserView.php ├── index.php └── settings └── index.php /AllScan-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/AllScan-logo.png -------------------------------------------------------------------------------- /AllScan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/AllScan.png -------------------------------------------------------------------------------- /AllScanInstallUpdate.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 15 | exit(0); 16 | } 17 | 18 | // Check if this file is the most recent version 19 | checkInstallerVersion(); 20 | 21 | // Determine web server folder 22 | $dirs = ['/var/www/html', '/srv/http']; 23 | foreach($dirs as $d) { 24 | if(is_dir($d)) { 25 | $webdir = $d; 26 | break; 27 | } 28 | } 29 | msg("Web Server Folder: " . (isset($webdir) ? $webdir : "Not Found.")); 30 | 31 | // Determine web server group name 32 | $name = ['www-data', 'http', 'apache']; 33 | foreach($name as $n) { 34 | if(`grep "^$n:" /etc/group`) { 35 | $group = $n; 36 | break; 37 | } 38 | } 39 | msg("Web Group Name: " . (isset($group) ? $group : "Not Found.")); 40 | 41 | if(!isset($webdir) || !isset($group)) 42 | exit(); 43 | 44 | // cd to web root folder 45 | $cwd = getcwd(); 46 | if($cwd !== $webdir) { 47 | msg("Changing dir from $cwd to $webdir"); 48 | chdir($webdir); 49 | $cwd = getcwd(); 50 | if($cwd !== $webdir) 51 | errExit("cd failed."); 52 | } 53 | 54 | // Destination dir in web root folder: 55 | $asdir = 'allscan'; 56 | // Enable mkdir(..., 0775) to work properly 57 | umask(0002); 58 | clearstatcache(); 59 | // Check if dir exists. If so, see if an update is needed. If not, install AllScan 60 | $dlfiles = true; 61 | $ver = 'Unknown'; 62 | if(is_dir($asdir)) { 63 | msg("$asdir dir exists. Checking if update needed..."); 64 | if(checkUpdate($ver)) { 65 | msg("AllScan is out-of-date."); 66 | $s = readline("Ready to Update AllScan. Enter 'y' to confirm, any other key to exit: "); 67 | if($s !== 'y') 68 | exit(); 69 | $bak = "$asdir.bak.$ver"; 70 | msg("Moving $asdir/ to $bak/..."); 71 | if(is_dir($bak)) 72 | execCmd("rm -rf $bak"); 73 | execCmd("mv $asdir $bak"); 74 | } else { 75 | msg("AllScan is up-to-date."); 76 | $dlfiles = false; 77 | } 78 | } else { 79 | msg("$asdir dir not found."); 80 | $s = readline("Ready to Install AllScan. Enter 'y' to confirm, any other key to exit: "); 81 | if($s !== 'y') 82 | exit(); 83 | } 84 | 85 | if($dlfiles) { 86 | $fname = 'main.zip'; 87 | $url = 'https://github.com/davidgsd/AllScan/archive/refs/heads/' . $fname; 88 | $zdir = 'AllScan-main'; 89 | if(file_exists($fname)) 90 | unlink($fname); 91 | if(is_dir($zdir)) 92 | exec("rm -rf $zdir"); 93 | if(!execCmd("wget -q '$url'") || !file_exists($fname)) 94 | errExit("Retrieve $fname from github failed. Try executing \"wget '$url'\" and check error messages. Also confirm that your node supports https and that its system time/RTC is set correctly."); 95 | if(!execCmd("unzip -q $fname")) 96 | errExit('Unzip failed. Check that you have unzip installed. Try "sudo apt-get install unzip" to install'); 97 | unlink($fname); 98 | if(!rename($zdir, $asdir)) 99 | msg("ERROR: mv($zdir, $asdir) failed"); 100 | // Copy any user .ini files from old version backup folder 101 | if(isset($bak) && is_dir($bak)) { 102 | msg("Checking for .ini files in $bak/..."); 103 | execCmd("cp -n $bak/*.ini $asdir/"); 104 | } 105 | } 106 | 107 | msg("Verifying $asdir dir has $group group writable permissions"); 108 | if((fileperms($asdir) & 0777) != 0775) 109 | execCmd("chmod 775 $asdir"); 110 | if(getGroupName($asdir) !== $group) 111 | execCmd("chgrp $group $asdir"); 112 | $inis = glob("$asdir/*{.ini,.ini.bak}", GLOB_BRACE); 113 | if(!empty($inis)) { 114 | foreach($inis as $f) { 115 | if((fileperms($f) & 0777) != 0664) 116 | execCmd("chmod 664 $f"); 117 | if(getGroupName($f) !== $group) 118 | execCmd("chgrp $group $f"); 119 | } 120 | } 121 | 122 | checkDbDir(); 123 | checkSmDir(); 124 | 125 | msg("PHP Version: " . phpversion()); 126 | $astver = trim(shell_exec('asterisk -V') ?? 'Unknown'); 127 | msg("Asterisk/ASL Version: " . $astver); 128 | 129 | // Confirm necessary php extensions are installed 130 | msg("Checking OS packages and PHP extensions..."); 131 | if(!class_exists('SQLite3')) { 132 | msg("Required SQLite3 Class not found." . NL 133 | ."Try running the update commands below to update your OS and php-sqlite3 package." . NL 134 | ."You may also need to enable the pdo_sqlite and sqlite3 extensions in php.ini."); 135 | } 136 | 137 | msg("Would you like to check for OS/package updates?" . NL 138 | ."If you recently updated your system or if everything is working great you do not need to do this now. " . NL 139 | ."The update process can cause some software packages to stop working or require config file updates." . NL 140 | ."DO NOT PROCEED WITH THIS STEP IF YOU'RE NOT SURE OR ARE NOT AN AUTHORIZED SERVER ADMIN." . NL 141 | ."Otherwise it is recommended to ensure your system is up-to-date."); 142 | $s = readline("Enter 'y' to proceed, any other key to skip this step: "); 143 | if($s === 'y') { 144 | if(is_executable('/usr/bin/apt-get')) { 145 | passthruCmd("apt-get -y update"); 146 | passthruCmd("apt-get upgrade"); 147 | } elseif(is_executable('/usr/bin/yum')) { 148 | passthruCmd("yum -y update"); 149 | passthruCmd("yum upgrade"); 150 | } elseif(is_executable('/usr/bin/pacman')) { 151 | passthruCmd("pacman -Syu"); 152 | passthruCmd("pacman -S php-sqlite"); 153 | } 154 | restartWebServer(); 155 | } 156 | 157 | // if ASL3, make sure astdb.txt is available, and sqlite3 and avahi-daemon are set up 158 | if(is_executable('/usr/bin/apt')) { 159 | if(!is_file('/etc/systemd/system/asl3-update-astdb.service')) { 160 | passthruCmd("sudo apt install -y asl3-update-nodelist 2> /dev/null"); 161 | } 162 | $fc = exec("find /var/lib/php -name sqlite3 -type f -printf '.'| wc -c"); 163 | if(!$fc) { 164 | passthruCmd("sudo apt install -y php-sqlite3 2> /dev/null"); 165 | restartWebServer(); 166 | } 167 | if(!is_executable('/usr/bin/asl-tts')) { 168 | passthruCmd("sudo apt install -y asl3-tts 2> /dev/null"); 169 | } 170 | if(!is_executable('/usr/sbin/avahi-daemon')) { 171 | passthruCmd("sudo apt install -y avahi-daemon 2> /dev/null"); 172 | } 173 | sleep(1); 174 | if(is_file('/etc/systemd/system/asl3-update-astdb.service')) { 175 | if(exec('systemctl is-enabled asl3-update-astdb.service') !== "enabled") 176 | passthruCmd("systemctl enable asl3-update-astdb.service 2> /dev/null"); 177 | if(exec('systemctl is-enabled asl3-update-astdb.timer') !== "enabled") { 178 | passthruCmd("systemctl enable asl3-update-astdb.timer 2> /dev/null"); 179 | passthruCmd("systemctl start asl3-update-astdb.timer 2> /dev/null"); 180 | } 181 | // Make a readable copy of allmon3.ini (Allmon3 updates can reset the file permissions) 182 | $fname = '/etc/allmon3/allmon3.ini'; 183 | if(file_exists($fname)) { 184 | $fname2 = '/etc/asterisk/allmon.ini.php'; 185 | if(!file_exists($fname2) || exec("diff $fname $fname2")) 186 | execCmd("cp $fname $fname2"); 187 | if((fileperms($fname2) & 0777) != 0660) 188 | execCmd("chmod 660 $fname2"); 189 | if(getGroupName($fname2) !== $group) 190 | execCmd("chgrp $group $fname2"); 191 | } 192 | } 193 | } 194 | 195 | // Confirm SQLite3 is enabled in php.ini 196 | $fn = exec('sudo find /etc/php -name php.ini |grep -v cli'); 197 | if($fn) { 198 | msg("php.ini location: $fn"); 199 | $lcgood = exec("grep sqlite /etc/php/8.2/apache2/php.ini | grep '^extension=' | wc -l"); 200 | $lcbad = exec("grep sqlite /etc/php/8.2/apache2/php.ini | grep '^;extension=' | wc -l"); 201 | if($lcgood >= 2) 202 | msg("php.ini appears to have SQLite3 enabled"); 203 | elseif($lcbad < 2) { 204 | msg("\nWARNING: SQLite3 extension does not appear to be enabled in php.ini.\n" 205 | ."You may need to manually edit the file and uncomment (remove leading ';') " 206 | ."the lines that say 'extension=pdo_sqlite' and 'extension=sqlite3'."); 207 | $s = readline("Hit any key to confirm"); 208 | } else { 209 | msg("Backing up php.ini -> $fn.bak"); 210 | execCmd("cp $fn $fn.bak"); 211 | msg("Enabling SQLite3 extension in php.ini"); 212 | execCmd("sed -i 's/;extension=pdo_sqlite/extension=pdo_sqlite/g' $fn"); 213 | execCmd("sed -i 's/;extension=sqlite3/extension=sqlite3/g' $fn"); 214 | restartWebServer(); 215 | } 216 | } else { 217 | msg("php.ini not found in /etc/php/"); 218 | } 219 | 220 | // Check DTMF command script and audio files 221 | $d0 = "$webdir/$asdir/_tools/agi-bin"; 222 | $d1 = "/usr/share/asterisk/agi-bin"; 223 | $ls0 = trim(shell_exec("ls $d0 2>/dev/null")); 224 | if(!$ls0) { 225 | msg("Error reading $d0/."); 226 | } else { 227 | if(!file_exists($d1)) { 228 | msg("$d1/ not found."); 229 | } else { 230 | $f0 = explode(NL, $ls0); 231 | sort($f0); 232 | $ls1 = trim(shell_exec("ls $d1 2>/dev/null")); 233 | $f1 = $ls1 ? explode(NL, $ls1) : []; 234 | sort($f1); 235 | $fcp = []; 236 | $ov = 0; 237 | foreach($f0 as $f) { 238 | if(array_search($f, $f1) === false) { 239 | $fcp[] = $f; 240 | } elseif(exec("diff $d0/$f $d1/$f")) { 241 | $fcp[] = $f; 242 | $ov++; 243 | } 244 | } 245 | if(count($fcp)) { 246 | msg("Copy DTMF command support files to /usr/share/asterisk/agi-bin/?\n" 247 | ."Files to be copied: " . implode(', ', $fcp)); 248 | if($ov) 249 | msg("Warning: This will result in $ov file(s) being overwritten"); 250 | if(count($f1)) 251 | msg("Existing files in directory: " . implode(', ', $f1)); 252 | $s = readline("Enter 'y' to confirm, any other key to skip: "); 253 | if($s === 'y') { 254 | foreach($fcp as $f) 255 | execCmd("cp $d0/$f $d1/$f"); 256 | } 257 | } 258 | } 259 | msg("See $webdir/$asdir/docs/rpt.conf and extensions.conf for DTMF command setup notes."); 260 | } 261 | 262 | msg("Install/Update Complete."); 263 | 264 | // Show URLs where AllScan can be accessed and other notes 265 | $nn = exec("grep '^NODE = ' /etc/asterisk/extensions.conf"); 266 | $bp = exec("grep '^bindport = ' /etc/asterisk/iax.conf"); 267 | $ip = exec("wget -t 1 -T 3 -q -O- http://checkip.dyndns.org:8245 | cut -d':' -f2 | cut -d' ' -f2 | cut -d'<' -f1"); 268 | $hn = exec('hostname'); 269 | $lanip = exec("hostname -I | cut -f1 -d' '"); 270 | if(!filter_var($lanip, FILTER_VALIDATE_IP)) { 271 | $lanip = exec("ifconfig | grep inet | head -1 | awk '{print $2}'"); 272 | if($lanip === '127.0.0.1') 273 | $lanip = exec("ifconfig | grep inet | tail -1 | awk '{print $2}'"); 274 | } 275 | $lip = ''; 276 | if($lanip) 277 | $lip = "http://$lanip/$asdir/"; 278 | if($hn) { 279 | if($lip) 280 | $lip .= ' or '; 281 | $lip .= "http://$hn.local/$asdir/"; 282 | } 283 | if(!$lip) 284 | $lip = '[Local IPV4 address / hostname could not be determined]'; 285 | 286 | $wip = "http://$ip/$asdir/"; 287 | if(preg_match('/NODE = ([0-9]{4,6})/', $nn, $m) == 1) { 288 | $node = $m[1]; 289 | $wip .= " or http://$node.nodes.allstarlink.org/$asdir/"; 290 | } 291 | $port = 4569; 292 | if(preg_match('/bindport = ([0-9]{4,5})/', $bp, $m) == 1) { 293 | $port = $m[1]; 294 | } 295 | 296 | msg("AllScan can be accessed at:\n\t$lip on the local network, or\n" 297 | ."\t$wip remotely\n\tif your router has port $port forwarded to this node."); 298 | 299 | if($dlfiles) { 300 | msg("Be sure to bookmark the above URL(s) in your browser."); 301 | msg("IMPORTANT: After updates do a CTRL-F5 in your browser (or long-press the reload button in mobile\n" 302 | ."browsers), or clear the browser cache, so that CSS and JavaScript files will properly update."); 303 | } 304 | 305 | exit(); 306 | 307 | // --------------------------------------------------- 308 | 309 | function checkUpdate(&$ver) { 310 | global $asdir; 311 | $fname = "include/common.php"; 312 | $vpat = '/^\$AllScanVersion = "v([0-9\.]{3,4})"/'; 313 | if(!file_exists("$asdir/$fname")) 314 | return true; 315 | $file = file("$asdir/$fname"); 316 | if(empty($file)) 317 | return true; 318 | foreach($file as $line) { 319 | if(preg_match($vpat, $line, $m) == 1) { 320 | $ver = $m[1]; 321 | break; 322 | } 323 | } 324 | msg("AllScan current version: $ver"); 325 | if($ver < 0.93) 326 | return true; 327 | 328 | $url = "https://raw.githubusercontent.com/davidgsd/AllScan/main/$fname"; 329 | $file = file($url); 330 | if(empty($file)) 331 | errExit("Retrieve $fname from github failed. Try executing \"wget '$url'\" and check error messages. Also confirm that your node supports https and that its system time/RTC is set correctly."); 332 | 333 | foreach($file as $line) { 334 | if(preg_match($vpat, $line, $m) == 1) { 335 | $gver = $m[1]; 336 | break; 337 | } 338 | } 339 | if(empty($gver)) 340 | errExit("Error parsing $url. Please visit $asurl and follow the install/update instructions."); 341 | 342 | msg("AllScan github version: $gver"); 343 | return ($ver < $gver); 344 | } 345 | 346 | function checkInstallerVersion() { 347 | global $AllScanInstallerUpdaterVersion; 348 | msg("AllScan Installer/Updater Version: $AllScanInstallerUpdaterVersion"); 349 | $fname = basename(__FILE__); 350 | msg("Checking github version..."); 351 | $url = "https://raw.githubusercontent.com/davidgsd/AllScan/main/$fname"; 352 | $asurl = 'https://github.com/davidgsd/AllScan'; 353 | $file = file($url); 354 | if(empty($file)) 355 | errExit("Retrieve $fname from github failed. Try executing \"wget '$url'\" and check error messages. Also confirm that your node supports https and that its system time/RTC is set correctly."); 356 | 357 | foreach($file as $line) { 358 | if(preg_match('/^\$AllScanInstallerUpdaterVersion = "([0-9\.]{3,4})"/', $line, $m) == 1) { 359 | $gver = $m[1]; 360 | break; 361 | } 362 | } 363 | if(empty($gver)) 364 | errExit("Error parsing $url. Please visit $asurl and follow the install/update instructions there."); 365 | 366 | msg("AllScan Installer/Updater github version: $gver"); 367 | if($AllScanInstallerUpdaterVersion != $gver) 368 | errExit("This file is out-of-date. Please visit $asurl and follow the install/update instructions there."); 369 | } 370 | 371 | // Execute command, show the command, show the output and return val 372 | function execCmd($cmd) { 373 | echo "Executing cmd: $cmd ... "; 374 | $out = ''; 375 | $res = 0; 376 | $ok = (exec($cmd, $out, $res) !== false && !$res); 377 | $s = $ok ? 'OK' : 'ERROR'; 378 | msg($s); 379 | return $ok; 380 | } 381 | 382 | function passthruCmd($cmd) { 383 | msg("Executing cmd: $cmd"); 384 | $res = 0; 385 | $ok = (passthru($cmd, $res) !== false && !$res); 386 | $s = $ok ? 'OK' : 'ERROR'; 387 | msg("Return Code: $s"); 388 | return $ok; 389 | } 390 | 391 | function checkDbDir() { 392 | global $group, $ver; 393 | // Confirm /etc/allscan dir exists and is writable by web server 394 | $asdbdir = '/etc/allscan'; 395 | if(!is_dir($asdbdir)) 396 | execCmd("mkdir $asdbdir"); 397 | if((fileperms($asdbdir) & 0777) != 0775) 398 | execCmd("chmod 775 $asdbdir"); 399 | if(getGroupName($asdbdir) !== $group) 400 | execCmd("chgrp $group $asdbdir"); 401 | // Backup DB file 402 | $dbfile = $asdbdir . '/allscan.db'; 403 | if(!$ver) 404 | $ver = 'bak'; 405 | $bakfile = "$dbfile.$ver"; 406 | if(file_exists($dbfile) && !file_exists($bakfile)) 407 | execCmd("cp $dbfile $bakfile"); 408 | } 409 | 410 | function checkSmDir() { 411 | global $group; 412 | // Verify supermon folder favorites.ini and favorites.ini.bak writable by web server 413 | $smdir = 'supermon'; 414 | if(is_dir($smdir)) { 415 | $favsini = 'favorites.ini'; 416 | $favsbak = "$favsini.bak"; 417 | msg("Confirming supermon $favsini and $favsbak writable by web server"); 418 | chdir($smdir); 419 | 420 | if(!file_exists($favsini)) 421 | execCmd("touch $favsini"); 422 | if(!file_exists($favsbak)) 423 | execCmd("touch $favsbak"); 424 | 425 | if((fileperms($favsini) & 0777) != 0664) 426 | execCmd("chmod 664 $favsini"); 427 | if((fileperms($favsbak) & 0777) != 0664) 428 | execCmd("chmod 664 $favsbak"); 429 | if((fileperms('.') & 0777) != 0775) 430 | execCmd("chmod 775 ."); 431 | 432 | if(getGroupName($favsini) !== $group) 433 | execCmd("chgrp $group $favsini"); 434 | if(getGroupName($favsbak) !== $group) 435 | execCmd("chgrp $group $favsbak"); 436 | if(getGroupName('.') !== $group) 437 | execCmd("chgrp $group ."); 438 | 439 | chdir('..'); 440 | } 441 | } 442 | 443 | function restartWebServer() { 444 | msg("Restarting web server..."); 445 | if(is_executable('/usr/bin/apachectl') || is_executable('/usr/sbin/apachectl')) 446 | $cmd = "apachectl restart 2> /dev/null"; 447 | else 448 | $cmd = "systemctl restart lighttpd.service 2> /dev/null"; 449 | if(!execCmd($cmd)) 450 | msg("Restart webserver or restart node now"); 451 | } 452 | 453 | function getGroupName($f) { 454 | if(!($id = filegroup($f))) 455 | return ''; 456 | $a = posix_getgrgid($id); 457 | return $a['name'] ?? ''; 458 | } 459 | 460 | function msg($s) { 461 | echo $s . NL; 462 | } 463 | 464 | function errExit($s) { 465 | msg("\nERROR: $s\n"); 466 | exit(); 467 | } 468 | -------------------------------------------------------------------------------- /_tools/agi-bin/getip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo SET VARIABLE result `hostname -I | cut -f1 -d' '` 3 | -------------------------------------------------------------------------------- /_tools/agi-bin/poweroff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo /usr/sbin/poweroff 3 | -------------------------------------------------------------------------------- /_tools/agi-bin/poweroff.ul: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/_tools/agi-bin/poweroff.ul -------------------------------------------------------------------------------- /_tools/agi-bin/reboot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo /usr/sbin/reboot 3 | -------------------------------------------------------------------------------- /_tools/agi-bin/reboot.ul: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/_tools/agi-bin/reboot.ul -------------------------------------------------------------------------------- /_tools/agi-bin/sayip.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 17 | exit(0); 18 | } 19 | $exts = ['c', 'h', 'php', 'txt', 'md', 'sh', 'html', 'conf']; 20 | $recursive = false; 21 | // Drop first element off of argv and argc (command text) 22 | $argc--; 23 | array_shift($argv); 24 | // Check for -r option 25 | if(isset($argv[0]) && $argv[0] === '-r') { 26 | $recursive = true; 27 | $argc--; 28 | array_shift($argv); 29 | } 30 | // Get search words 31 | $n = count($argv); 32 | if($n < 1) { 33 | echo "No search words specified.\n"; 34 | exit(0); 35 | } 36 | $searchwords = $argv; 37 | // Change to pwd (script starts in dir where it is located, not where called from) 38 | $pwd = getcwd(); 39 | chdir($pwd); 40 | //echo "pwd: $pwd\n"; 41 | $subdirs = []; 42 | // Combine words into search pattern 43 | function escapeVal(&$val, $key) { $val = preg_quote($val, '/'); } 44 | array_walk($searchwords, 'escapeVal'); 45 | $pattern = '('.implode('|', $searchwords).')'; 46 | $level=0; 47 | searchDir('.', $subdirs, $pattern, $recursive); 48 | exit(0); 49 | 50 | function searchDir($dir, $subdirs, $pattern, $recursive) { 51 | global $exts, $pwd, $level; 52 | $level++; 53 | if($handle = opendir($dir)) { 54 | while(($file = readdir($handle)) !== false) { 55 | // If is a directory, dir not excluded, and recursion enabled, call this function recursively 56 | if(is_dir($file)) { 57 | if($recursive && !in_array($file, ['.', '..']) && filetype($file) !== 'link' && 58 | (empty($subdirs) || in_array($file, $subdirs))) { 59 | chdir($file); 60 | // Do not recurse more than 3 levels 61 | searchDir('.', null, $pattern, ($level < 3)); 62 | chdir(".."); 63 | } 64 | } else { 65 | // Check file extension 66 | $path_parts = pathinfo($file); 67 | if(isset($path_parts['extension']) && in_array($path_parts['extension'], $exts)) { 68 | // Search file 69 | $contents = file($file); 70 | $results = preg_grep("/$pattern/", $contents); 71 | if(!empty($results)) { 72 | // Output results 73 | //var_dump($results); 74 | $path = getcwd() . "/" . $file; 75 | if(strpos($path, $pwd) === 0) { 76 | $path = '.' . substr($path, strlen($pwd)); 77 | } elseif(preg_match("|/var/www/html/(.*)|", $path, $matches) == 1 || 78 | preg_match("|/home/allscan/(.*)|", $path, $matches) == 1) { 79 | $path = $matches[1]; 80 | } 81 | $lines = array_keys($results); 82 | foreach($lines as $line) 83 | printf("%s:%s: %s\n", $path, $line, trim($results[$line])); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | closedir($handle); 90 | $level--; 91 | } 92 | -------------------------------------------------------------------------------- /_tools/checkFs.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | 50MB in size in /var/log/ or /var/log/asterisk/ 20 | checkDiskSpace($msg); 21 | if($showMsgs) 22 | echo implode(NL, $msg) . NL; 23 | if($cwd !== __DIR__) 24 | chdir($cwd); 25 | exit(); 26 | -------------------------------------------------------------------------------- /_tools/copyFromWww.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 5 | exit(0); 6 | } 7 | $webRoot = '/var/www/html/allscan/'; 8 | // Look in web folder and copy all files to local dir, with exception of the following dirs: 9 | $excludes = ['test', 'old', '*.tmp', '*.bak', 'log.txt']; 10 | // Note: pwd starts in dir where script is located, not where called from 11 | // Do below if might be called from somewhere else 12 | $pwd = getcwd(); 13 | chdir($pwd); 14 | echo "pwd = $pwd, webRoot = $webRoot\n"; 15 | if(!is_dir($webRoot)) { 16 | echo "webRoot dir not found\n"; 17 | exit(1); 18 | } 19 | if($webRoot === $pwd) { 20 | echo "Can't run script from web root folder. Go to dev folder.\n"; 21 | exit(1); 22 | } 23 | 24 | copyFiles($excludes, false, false); 25 | 26 | //--- functions --- 27 | function copyFiles($excludes, $test=false, $toWww=true) { 28 | global $webRoot; 29 | echo "Copying development files ". ($toWww ? 'TO' : 'FROM') . " www dir...\n\n"; 30 | $exclude = ''; 31 | if(!empty($excludes)) { 32 | foreach($excludes as $dir) 33 | $exclude .= "--exclude='$dir' "; 34 | } 35 | $opts = $test ? '--list-only' : ''; 36 | $dirs = $toWww ? '. ' . $webRoot : $webRoot . ' .'; 37 | $cmd = "rsync -avC $exclude $opts $dirs"; 38 | passthru($cmd); 39 | echo "\nDone\n"; 40 | } 41 | -------------------------------------------------------------------------------- /_tools/copyToWww.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 5 | exit(0); 6 | } 7 | $webRoot = '/var/www/html/allscan/'; 8 | // Copy files to webroot, with exception of the following dirs/files: 9 | $excludes = array('.git', '.gitignore'); 10 | 11 | // Note: pwd starts in dir where script is located, not where called from 12 | // Do below if might be called from somewhere else 13 | $pwd = getcwd(); 14 | chdir($pwd); 15 | echo "pwd = $pwd, webRoot = $webRoot\n"; 16 | if($webRoot === $pwd) { 17 | echo "Can't run script from web root folder. Go to dev folder.\n"; 18 | exit(1); 19 | } 20 | copyFiles($excludes, false); 21 | 22 | //--- functions --- 23 | function copyFiles($excludes, $test=false, $toWww=true) { 24 | global $webRoot; 25 | $exclude = ''; 26 | if(!empty($excludes)) { 27 | foreach($excludes as $pat) 28 | $exclude .= "--exclude='$pat' "; 29 | } 30 | $opts = $test ? '--list-only' : ''; 31 | $dirs = $toWww ? '. ' . $webRoot : $webRoot . ' .'; 32 | $cmd = "rsync -avC $exclude $opts $dirs"; 33 | passthru($cmd); 34 | echo "Done\n"; 35 | } 36 | -------------------------------------------------------------------------------- /_tools/diffWww.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 5 | exit(0); 6 | } 7 | $webRoot = '/var/www/html/allscan/'; 8 | // Note: pwd starts in dir where script is located, not where called from 9 | // Do below if might be called from somewhere else 10 | $pwd = getcwd(); 11 | chdir($pwd); 12 | echo "pwd = $pwd, webRoot = $webRoot\n"; 13 | if(!is_dir($webRoot)) { 14 | echo "webRoot dir not found\n"; 15 | exit(1); 16 | } 17 | if($webRoot === $pwd) { 18 | echo "Can't run script from web root. Go to dev folder.\n"; 19 | exit(1); 20 | } 21 | 22 | diffFiles(); 23 | 24 | //--- functions --- 25 | function diffFiles() { 26 | global $webRoot; 27 | echo "Diff'ing development files vs. www dir...\n\n"; 28 | $exclude = '-X _tools/excludeFiles.txt'; 29 | $dirs = '. ' . $webRoot; 30 | $cmd = "diff -r $exclude $dirs"; 31 | passthru($cmd); 32 | echo "\nDone\n"; 33 | } 34 | -------------------------------------------------------------------------------- /_tools/excludeFiles.txt: -------------------------------------------------------------------------------- 1 | msg 2 | old 3 | downloads 4 | *.tmp 5 | *.bak 6 | *log.txt 7 | *Log.txt 8 | -------------------------------------------------------------------------------- /_tools/setUpDtmfCmds.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 18 | exit(0); 19 | } 20 | 21 | $dir1 = 'agi-bin'; 22 | $dir2 = '/usr/share/asterisk/agi-bin'; 23 | if(!is_dir($dir1)) 24 | errExit("Dir '$dir1' not found."); 25 | 26 | if(!is_dir($dir2)) 27 | errExit("Dir '$dir2' not found."); 28 | 29 | msg("Copying .sh and .ul files from '$dir1' to '$dir2'..."); 30 | 31 | if(!passthruCmd("ls -l $dir1/")) 32 | errExit('ls failed'); 33 | 34 | if(!passthruCmd("ls -l $dir2/")) 35 | errExit('ls failed'); 36 | 37 | if(!passthruCmd("cp $dir1/* $dir2/")) 38 | errExit('cp failed'); 39 | 40 | msg("Updating file permissions..."); 41 | 42 | if(!passthruCmd("chmod 755 $dir2/*.sh")) 43 | errExit('chmod failed'); 44 | 45 | if(!passthruCmd("chmod 644 $dir2/*.ul")) 46 | errExit('chmod failed'); 47 | 48 | if(!passthruCmd("ls -l $dir2/")) 49 | 50 | msg('Files copied successfully. See comments in this file for text to add to rpt.conf and extensions.conf'); 51 | 52 | exit(); 53 | 54 | // --------------------------------------------------- 55 | // Execute command, show the command, show the output and return val 56 | function execCmd($cmd) { 57 | msg("Executing cmd: $cmd"); 58 | $out = ''; 59 | $res = 0; 60 | $ok = (exec($cmd, $out, $res) !== false && !$res); 61 | $s = $ok ? 'OK' : 'ERROR'; 62 | msg("Return Code: $s"); 63 | return $ok; 64 | } 65 | 66 | function passthruCmd($cmd) { 67 | msg("Executing cmd: $cmd"); 68 | $res = 0; 69 | $ok = (passthru($cmd, $res) !== false && !$res); 70 | $s = $ok ? 'OK' : 'ERROR'; 71 | msg("Return Code: $s"); 72 | return $ok; 73 | } 74 | 75 | function msg($s) { 76 | echo $s . NL; 77 | } 78 | 79 | function errExit($s) { 80 | msg('ERROR: ' . $s); 81 | msg('Check directory permissions and that this script was run as sudo/root.'); 82 | exit(); 83 | } 84 | -------------------------------------------------------------------------------- /_tools/wgrep.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | \n"; 19 | exit(0); 20 | } 21 | $exts = ['htm', 'html', 'php', 'txt', 'css', 'js']; 22 | $recursive = false; 23 | // Drop first element off of argv and argc (command text) 24 | $argc--; 25 | array_shift($argv); 26 | // Check for -r option 27 | if(isset($argv[0]) && $argv[0] === '-r') { 28 | $recursive = true; 29 | $argc--; 30 | array_shift($argv); 31 | } 32 | // Get search words 33 | $n = count($argv); 34 | if($n < 1) { 35 | echo "No search words specified.\n"; 36 | exit(0); 37 | } 38 | $searchwords = $argv; 39 | // Change to pwd (script starts in dir where it is located, not where called from) 40 | $pwd = getcwd(); 41 | chdir($pwd); 42 | //echo "pwd: $pwd\n"; 43 | $subdirs = []; 44 | // Combine words into search pattern 45 | function escapeVal(&$val, $key) { $val = preg_quote($val, '/'); } 46 | array_walk($searchwords, 'escapeVal'); 47 | $pattern = '('.implode('|', $searchwords).')'; 48 | searchDir('.', $subdirs, $pattern, $recursive); 49 | exit(0); 50 | 51 | function searchDir($dir, $subdirs, $pattern, $recursive) { 52 | global $exts; 53 | if($handle = opendir($dir)) { 54 | while(($file = readdir($handle)) !== false) { 55 | // If is a directory, dir not excluded, and recursion enabled, call this function recursively 56 | if(is_dir($file)) { 57 | if($recursive && !in_array($file, ['.', '..']) && filetype($file) !== 'link' && 58 | (empty($subdirs) || in_array($file, $subdirs))) { 59 | chdir($file); 60 | // For now do not recurse more than 1 level deep 61 | searchDir('.', null, $pattern, false); 62 | chdir(".."); 63 | } 64 | } else { 65 | // Check file extension 66 | $path_parts = pathinfo($file); 67 | if(isset($path_parts['extension']) && in_array($path_parts['extension'], $exts)) { 68 | // Search file 69 | $contents = file($file); 70 | $results = preg_grep("/$pattern/", $contents); 71 | if(!empty($results)) { 72 | // Output results 73 | //var_dump($results); 74 | $path = getcwd() . "/" . $file; 75 | if(preg_match("|/var/www/html/(.*)|", $path, $matches) == 1) 76 | $path = $matches[1]; 77 | else if(preg_match("|/home/repeater/(.*)|", $path, $matches) == 1) 78 | $path = $matches[1]; 79 | $lines = array_keys($results); 80 | foreach($lines as $line) 81 | printf("%s:%s: %s\n", $path, $line, trim($results[$line])); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | closedir($handle); 88 | } 89 | -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | true, 'data' => cpuTemp()], $f); 28 | break; 29 | } 30 | } 31 | 32 | function sendData($data, $event='errMsg') { 33 | $resp = ['event' => $event, 'data' => $data]; 34 | echo json_encode($resp); 35 | ob_flush(); 36 | flush(); 37 | } 38 | -------------------------------------------------------------------------------- /astapi/AMI.php: -------------------------------------------------------------------------------- 1 | getResponse($fp, $actionID); 19 | // logToFile('RES: ' . varDumpClean($res, true), AMI_DEBUG_LOG); 20 | $ok = (strpos($res[2], "Authentication accepted") !== false); 21 | // Determine App-rpt version. ASL3 and Asterisk 20 have some differences in AMI commands 22 | // eg. in ASL2 restart command is "restart now" but in ASL3 it's "core restart now". 23 | $s = $this->command($fp, 'rpt show version'); 24 | if(preg_match('/app_rpt version: ([0-9\.]{1,9})/', $s, $m) == 1) 25 | $this->aslver = $m[1]; 26 | } 27 | 28 | function command($fp, $cmdString, $debug=false) { 29 | // Generate ActionID to associate with response 30 | $actionID = 'cpAction_' . mt_rand(); 31 | $ok = true; 32 | $msg = []; 33 | if((fwrite($fp, "ACTION: COMMAND\r\nCOMMAND: $cmdString\r\nActionID: $actionID\r\n\r\n")) > 0) { 34 | if($debug) 35 | logToFile('CMD: ' . $cmdString . ' - ' . $actionID, AMI_DEBUG_LOG); 36 | $res = $this->getResponse($fp, $actionID, $debug); 37 | if(!is_array($res)) 38 | return $res; 39 | // Check for Asterisk AMI Success/Error response 40 | foreach($res as $r) { 41 | if($r === 'Response: Error') 42 | $ok = false; 43 | elseif(preg_match('/Output: (.*)/', $r, $m) == 1) 44 | $msg[] = $m[1]; 45 | } 46 | if(_count($msg)) 47 | return implode(NL, $msg); 48 | if($ok) 49 | return 'OK'; 50 | return 'ERROR'; 51 | } 52 | return "Get node $cmdString failed"; 53 | } 54 | 55 | /* Example ASL2 AMI response: 56 | Response: Follows 57 | Privilege: Command 58 | ActionID: cpAction_... 59 | --END COMMAND-- 60 | Example ASL3 AMI response: 61 | Response: Success 62 | Command output follows 63 | Output: 64 | ActionID: cpAction_... 65 | => "Response:" line indicates success of associated ActionID. 66 | */ 67 | 68 | function getResponse($fp, $actionID, $debug=false) { 69 | $ignore = ['Privilege: Command', 'Command output follows']; 70 | $t0 = time(); 71 | $response = []; 72 | if($debug) 73 | $sn = getScriptName(); 74 | while(time() - $t0 < 20) { 75 | $str = fgets($fp); 76 | if($str === false) 77 | return $response; 78 | $str = trim($str); 79 | if($str === '') 80 | continue; 81 | if($debug) 82 | logToFile("$sn 1: $str", AMI_DEBUG_LOG); 83 | if(strpos($str, 'Response: ') === 0) { 84 | $response[] = $str; 85 | } elseif($str === "ActionID: $actionID") { 86 | $response[] = $str; 87 | while(time() - $t0 < 20) { 88 | $str = fgets($fp); 89 | if($str === "\r\n" || $str[0] === "\n" || $str === false) 90 | return $response; 91 | $str = trim($str); 92 | if($str === '' || in_array($str, $ignore)) 93 | continue; 94 | $response[] = $str; 95 | if($debug) 96 | logToFile("$sn 2: $str", AMI_DEBUG_LOG); 97 | } 98 | } 99 | } 100 | if(count($response)) 101 | return $response; 102 | logToFile("$sn: Timeout", AMI_DEBUG_LOG); 103 | return 'Timeout'; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /astapi/cmd.php: -------------------------------------------------------------------------------- 1 | connect($cfg[$localnode]['host']); 26 | if($fp === false) 27 | exit("Could not connect\n"); 28 | 29 | $amiuser = $cfg[$localnode]['user']; 30 | $pass = $cfg[$localnode]['passwd']; 31 | if($ami->login($fp, $amiuser, $pass) === false) 32 | exit("Could not login\n"); 33 | 34 | switch($button) { 35 | case 'dtmf': 36 | if(!preg_match("/^[\d*#,A-Da-d]+$/", $cmd)) 37 | exit("Invalid command value\n"); 38 | echo "Executing cmd \"$cmd\" on $localnode..."; 39 | $ctxt = "rpt fun $localnode $cmd"; 40 | break; 41 | case 'restart': 42 | echo "Restarting Asterisk..."; 43 | $ctxt = ($ami->aslver >= 3) ? "core restart now" : "restart now"; 44 | break; 45 | default: 46 | exit("Invalid command\n"); 47 | } 48 | $resp = $ami->command($fp, $ctxt); 49 | fclose($fp); 50 | echo $resp; 51 | -------------------------------------------------------------------------------- /astapi/connect.php: -------------------------------------------------------------------------------- 1 | connect($cfg[$localnode]['host']); 31 | if($fp === false) 32 | exit("Could not connect\n"); 33 | 34 | $amiuser = $cfg[$localnode]['user']; 35 | $pass = $cfg[$localnode]['passwd']; 36 | if($ami->login($fp, $amiuser, $pass) === false) 37 | exit("Could not login\n"); 38 | 39 | switch($button) { 40 | case 'connect': 41 | if($autodisc) { 42 | echo "Disconnect all nodes from $localnode..."; 43 | $resp = $ami->command($fp, "rpt cmd $localnode ilink 6 0"); 44 | echo $resp . BR; 45 | usleep(500000); 46 | } 47 | if($perm) { 48 | $ilink = 13; 49 | echo "Permanently Connect $localnode to $remotenode..."; 50 | } else { 51 | $ilink = 3; 52 | echo "Connect $localnode to $remotenode..."; 53 | } 54 | break; 55 | case 'monitor': 56 | if($perm) { 57 | $ilink = 12; 58 | echo "Permanently Monitor $remotenode from $localnode..."; 59 | } else { 60 | $ilink = 2; 61 | echo "Monitor $remotenode from $localnode..."; 62 | } 63 | break; 64 | case 'localmonitor': 65 | if($perm) { 66 | $ilink = 18; 67 | echo "Permanently Local Monitor $remotenode from $localnode..."; 68 | } else { 69 | $ilink = 8; 70 | echo "Local Monitor $remotenode from $localnode..."; 71 | } 72 | break; 73 | case 'disconnect': 74 | if($remotenode === '0') { 75 | $ilink = 6; 76 | echo "Disconnect all nodes from $localnode..."; 77 | } else { 78 | $ilink = 11; 79 | echo "Disconnect $remotenode from $localnode..."; 80 | } 81 | break; 82 | } 83 | $resp = $ami->command($fp, "rpt cmd $localnode ilink $ilink $remotenode"); 84 | fclose($fp); 85 | echo $resp; 86 | -------------------------------------------------------------------------------- /astapi/nodeInfo.php: -------------------------------------------------------------------------------- 1 | $info"; 11 | } elseif($nodeNum > 3000000) { 12 | $info = getEchoLinkInfo($fp, $nodeNum); 13 | } elseif(!empty($node['ip'])) { 14 | if(strlen(trim($node['ip'])) > 3) { 15 | $info = 'Web Txcvr / Phone Portal (' . $node['ip'] . ')'; 16 | } else { 17 | $info = 'Unknown Mode'; 18 | } 19 | } elseif(is_numeric($nodeNum)) { 20 | $info = 'Node not in database'; 21 | } elseif(`echo $nodeNum |egrep -c "\-P"` > 0) { 22 | $info = 'AllStar Phone Portal user'; 23 | } else { 24 | $info = 'IaxRpt / Web Transceiver client'; 25 | } 26 | return $info; 27 | } 28 | 29 | $elnk_cache = []; 30 | 31 | function getEchoLinkInfo($fp, $echonode) { 32 | global $elnk_cache, $ami; 33 | $lookup = (int)substr($echonode, 1); // Strip leading '3' and zeros 34 | if(isset($elnk_cache[$lookup])) { 35 | $column = $elnk_cache[$lookup]; 36 | $time = time(); 37 | if($time > $column[0]) 38 | unset($elnk_cache[$lookup]); 39 | } else { 40 | $AMI = $ami->command($fp, "echolink dbget nodename $lookup"); // Get EchoLink data from Asterisk 41 | $rows = explode("\n", $AMI); 42 | $column = explode("|", $rows[0]); 43 | if($column[0] == $lookup) { 44 | $column[0] = time() + 300; 45 | } else { 46 | $column = []; 47 | $column[0] = time() + 30; 48 | $column[1] = $column[2] = '-'; 49 | } 50 | $elnk_cache[$lookup] = $column; 51 | } 52 | // column[2] is EL node's IP Address which doesn't seem useful to show 53 | //$info = $column[1] . " [EchoLink $lookup] (" . $column[2] . ")"; 54 | $info = $column[1] . " [EchoLink $lookup]"; 55 | return $info; 56 | } 57 | -------------------------------------------------------------------------------- /astapi/server.php: -------------------------------------------------------------------------------- 1 | 'Insufficient user permission to retrieve data.']); 14 | exit(); 15 | } 16 | 17 | // Validate request 18 | if(empty($_GET['nodes'])) { 19 | sendData(['status' => 'Unknown request!']); 20 | exit(); 21 | } 22 | 23 | // Read parms 24 | $passedNodes = explode(',', trim(strip_tags($_GET['nodes']))); 25 | 26 | chdir('..'); 27 | 28 | // Load allmon.ini 29 | $cfg = readNodeCfg(); 30 | if($cfg === false) { 31 | sendData(['status' => "allmon.ini not found."]); 32 | exit(); 33 | } 34 | 35 | // Load ASL DB 36 | $astdb = readAstDb2(); 37 | 38 | // Verify nodes are in ini file 39 | $nodes = []; 40 | foreach($passedNodes as $i => $node) { 41 | if(isset($cfg[$node])) { 42 | $nodes[] = $node; 43 | } else { 44 | sendData(['node'=>$node, 'status'=>"Node $node not in allmon.ini"], 'nodes'); 45 | } 46 | } 47 | 48 | // Do not time out 49 | set_time_limit(0); 50 | 51 | // Open a socket to each Asterisk Manager 52 | $ami = new AMI(); 53 | $servers = []; 54 | $fp = []; 55 | $chandriver = 'Unknown'; 56 | $amicd = ''; 57 | $rxstatssupported = false; 58 | 59 | foreach($nodes as $node) { 60 | $host = $cfg[$node]['host']; 61 | if(!$host) { 62 | $data['status'] = "Invalid host setting in allmon.ini [$node]"; 63 | sendData($data, 'connection'); 64 | continue; 65 | } 66 | $data = ['host'=>$host, 'node'=>$node]; 67 | // Connect and login to each manager only once 68 | if(!array_key_exists($host, $servers)) { 69 | $data['status'] = "Connecting to Asterisk Manager $node $host..."; 70 | $fp[$host] = $ami->connect($host); 71 | if($fp[$host] === false) { 72 | $data['status'] .= 'Connect Failed. Check allmon.ini settings.'; 73 | } else { 74 | // Try to login 75 | $amiuser = $cfg[$node]['user']; 76 | $pass = $cfg[$node]['passwd']; 77 | if($ami->login($fp[$host], $amiuser, $pass) !== false) { 78 | $servers[$host] = 'y'; 79 | $data['status'] .= 'Login OK'; 80 | } else { 81 | $data['status'] .= "Login Failed. Check allmon.ini settings."; 82 | } 83 | // Log version info 84 | $data['status'] .= "
ASL Ver: $ami->aslver, AllScan Ver: " 85 | . substr($AllScanVersion, 1); 86 | // Check if rxaudiostats supported 87 | $msg = checkRxStatsSupport($ami, $fp[$host]); 88 | if(_count($msg)) 89 | $data['status'] .= BR . implode(BR, $msg); 90 | } 91 | sendData($data, 'connection'); 92 | } 93 | } 94 | 95 | // Main loop - build $data array and output as a json object 96 | $current = []; 97 | $saved = []; 98 | $nodeTime = []; 99 | //$n = 0; 100 | while(1) { 101 | foreach($nodes as $node) { 102 | // Is host of this node logged in? 103 | if(!isset($servers[$cfg[$node]['host']])) 104 | continue; 105 | $connectedNodes = getNode($fp[$cfg[$node]['host']], $node); 106 | $sortedConnectedNodes = sortNodes($connectedNodes); 107 | $info = getAstInfo($fp[$cfg[$node]['host']], $node); 108 | // Build array of time values 109 | $nodeTime[$node]['node'] = $node; 110 | $nodeTime[$node]['info'] = $info; 111 | // Build array 112 | $current[$node]['node'] = $node; 113 | $current[$node]['info'] = $info; 114 | // Save remote nodes 115 | $current[$node]['remote_nodes'] = []; 116 | $i = 0; 117 | foreach($sortedConnectedNodes as $arr) { 118 | // Store remote nodes time values 119 | $nodeTime[$node]['remote_nodes'][$i]['elapsed'] = $arr['elapsed'] ?? 0; 120 | $nodeTime[$node]['remote_nodes'][$i]['last_keyed'] = $arr['last_keyed']; 121 | // Store remote nodes other than time values 122 | // Array key of remote_nodes is not node number to prevent javascript (for in) sorting 123 | $current[$node]['remote_nodes'][$i]['node'] = $arr['node'] ?? ''; 124 | $current[$node]['remote_nodes'][$i]['info'] = $arr['info'] ?? ''; 125 | $current[$node]['remote_nodes'][$i]['link'] = ucwords(strtolower($arr['link'] ?? '')); 126 | $current[$node]['remote_nodes'][$i]['ip'] = $arr['ip'] ?? ''; 127 | $current[$node]['remote_nodes'][$i]['direction'] = $arr['direction'] ?? ''; 128 | $current[$node]['remote_nodes'][$i]['keyed'] = $arr['keyed'] ?? ''; 129 | $current[$node]['remote_nodes'][$i]['mode'] = $arr['mode'] ?? ''; 130 | $current[$node]['remote_nodes'][$i]['elapsed'] = ' '; 131 | $current[$node]['remote_nodes'][$i]['last_keyed'] = $arr['last_keyed'] === 'Never' ? 'Never' : NBSP; 132 | $current[$node]['remote_nodes'][$i]['cos_keyed'] = $arr['cos_keyed'] ?? 0; 133 | $current[$node]['remote_nodes'][$i]['tx_keyed'] = $arr['tx_keyed'] ?? 0; 134 | $current[$node]['remote_nodes'][$i]['lnodes'] = $arr['lnodes'] ?? []; 135 | $i++; 136 | } 137 | } 138 | // Send current nodes only when data changes 139 | if($current !== $saved) { 140 | sendData($current, 'nodes'); 141 | // if($n++ == 5) 142 | // logToFile($current, 'log.txt'); 143 | $saved = $current; 144 | } 145 | // Send times every cycle 146 | sendData($nodeTime, 'nodetimes'); 147 | // Wait 500mS 148 | usleep(500000); 149 | } 150 | 151 | fwrite($fp, "ACTION: Logoff\r\n\r\n"); 152 | 153 | exit(); 154 | 155 | function checkRxStatsSupport($ami, $fp) { 156 | global $chandriver, $amicd, $rxstatssupported; 157 | $res = $ami->command($fp, "susb tune menu-support Y"); 158 | if(strpos($res, 'RxAudioStats') === 0) { 159 | $rxstatssupported = true; 160 | $chandriver = 'Simpleusb'; 161 | $amicd = 'susb'; 162 | } 163 | if(!$rxstatssupported) { 164 | $res = $ami->command($fp, "radio tune menu-support Y"); 165 | if(strpos($res, 'RxAudioStats') === 0) { 166 | $rxstatssupported = true; 167 | $chandriver = 'Usbradio'; 168 | $amicd = 'radio'; 169 | } 170 | } 171 | if(!$rxstatssupported) { 172 | $msg[] = "ASL version does not support RxAudioStats"; 173 | } else { 174 | $msg[] = "RxAudioStats supported, $chandriver driver"; 175 | $res = $ami->command($fp, "$amicd show settings"); 176 | if(preg_match('/Card is ([-0-9]{1,2})/', $res, $m) == 1 && $m[1] >= 0) { 177 | $msg[] = "Channel driver settings:"; 178 | $ra = explode(NL, $res); 179 | foreach($ra as $m) { 180 | if(strposa($m, ['Output ', 'Rx ', 'Tx '])) 181 | $msg[] = $m; 182 | } 183 | } 184 | } 185 | return $msg; 186 | } 187 | 188 | // Get status for this $node 189 | function getNode($fp, $node) { 190 | global $ami; 191 | static $errCnt=0; 192 | $actionRand = mt_rand(); // Asterisk Manger Interface an actionID so we can find our own response 193 | $actionID = 'xstat' . $actionRand; 194 | if(fwrite($fp, "ACTION: RptStatus\r\nCOMMAND: XStat\r\nNODE: $node\r\nActionID: $actionID\r\n\r\n") !== false) { 195 | $rptStatus = $ami->getResponse($fp, $actionID); 196 | } else { 197 | sendData(['status'=>'XStat failed!']); 198 | // On ASL3 if Asterisk restarts above error repeats indefinitely. Let client JS reinit connection. 199 | if(++$errCnt > 9) 200 | exit(); 201 | } 202 | // format of Conn lines: Node# isKeyed lastKeySecAgo lastUnkeySecAgo 203 | $actionID = 'sawstat' . $actionRand; 204 | if(fwrite($fp, "ACTION: RptStatus\r\nCOMMAND: SawStat\r\nNODE: $node\r\nActionID: $actionID\r\n\r\n") !== false) { 205 | $sawStatus = $ami->getResponse($fp, $actionID); 206 | } else { 207 | sendData(['status'=>'sawStat failed!']); 208 | // On ASL3 if Asterisk restarts above error repeats indefinitely. Let client JS reinit connection. 209 | if(++$errCnt > 9) 210 | exit(); 211 | } 212 | // Returns an array of currently connected nodes 213 | $current = parseNode($fp, $rptStatus, $sawStatus); 214 | return $current; 215 | } 216 | 217 | function sendData($data, $event='errMsg') { 218 | echo "event: $event\n"; 219 | echo 'data: ' . json_encode($data) . "\n\n"; 220 | ob_flush(); 221 | flush(); 222 | } 223 | 224 | function sortNodes($nodes) { 225 | $arr = []; 226 | $notHeard = []; 227 | $sortedNodes = []; 228 | // Build arrays of heard and unheard 229 | foreach($nodes as $nodeNum => $row) { 230 | if(!isset($row['last_keyed']) || $row['last_keyed'] == '-1') { 231 | $notHeard[$nodeNum] = 'Never heard'; 232 | } else { 233 | $arr[$nodeNum] = $row['last_keyed']; 234 | } 235 | } 236 | // Sort nodes that have been heard 237 | if(count($arr) > 0) { 238 | asort($arr, SORT_NUMERIC); 239 | } 240 | // Add in nodes that have not been heard 241 | if(count($notHeard) > 0) { 242 | ksort($notHeard, SORT_NUMERIC); 243 | foreach($notHeard as $nodeNum => $row) { 244 | $arr[$nodeNum] = $row; 245 | } 246 | } 247 | // Build sorted node array 248 | foreach($arr as $nodeNum => $row) { 249 | // Build last_keyed string. Converts seconds to hours, minutes, seconds. 250 | if(isset($nodes[$nodeNum]['last_keyed']) && $nodes[$nodeNum]['last_keyed'] > -1) { 251 | $t = $nodes[$nodeNum]['last_keyed']; 252 | $h = floor($t / 3600); 253 | $m = floor(($t / 60) % 60); 254 | $s = $t % 60; 255 | $nodes[$nodeNum]['last_keyed'] = sprintf("%02d:%02d:%02d", $h, $m, $s); 256 | } else { 257 | $nodes[$nodeNum]['last_keyed'] = 'Never'; 258 | } 259 | $sortedNodes[$nodeNum] = $nodes[$nodeNum]; 260 | } 261 | return $sortedNodes; 262 | } 263 | 264 | function parseNode($fp, $rptStatus, $sawStatus) { 265 | $curNodes = []; 266 | $conns = []; // Directly connected nodes 267 | $lnodes = []; // All connected nodes 268 | $modes = []; 269 | // Parse 'rptStat Conn:' lines 270 | foreach($rptStatus as $line) { 271 | if(preg_match('/Conn: (.*)/', $line, $matches)) { 272 | $arr = preg_split("/\s+/", trim($matches[1])); 273 | if(is_numeric($arr[0]) && $arr[0] > 3000000) { 274 | // No IP w/EchoLink 275 | $conns[] = [$arr[0], "", $arr[1], $arr[2], $arr[3], $arr[4]]; 276 | } else { 277 | $conns[] = $arr; 278 | } 279 | } 280 | if(preg_match('/Var: RPT_RXKEYED=(.)/', $line, $matches)) { 281 | $rxKeyed = $matches[1]; 282 | } 283 | if(preg_match('/Var: RPT_TXKEYED=(.)/', $line, $matches)) { 284 | $txKeyed = $matches[1]; 285 | } 286 | if(preg_match("/LinkedNodes: (.*)/", $line, $matches)) { 287 | $longRangeLinks = preg_split("/, /", trim($matches[1])); 288 | foreach($longRangeLinks as $line) { 289 | $n = substr($line, 1); 290 | $modes[$n]['mode'] = substr($line, 0, 1); 291 | if(is_numeric($n) && $n >= 2000 && $n < 1000000) 292 | $lnodes[] = $n; 293 | } 294 | } 295 | } 296 | // Parse 'sawStat Conn:' lines 297 | $keyups = []; 298 | foreach($sawStatus as $line) { 299 | if(preg_match('/Conn: (.*)/', $line, $matches)) { 300 | $arr = preg_split("/\s+/", trim($matches[1])); 301 | $keyups[$arr[0]] = ['node' => $arr[0], 'isKeyed' => $arr[1], 'keyed' => $arr[2], 'unkeyed' => $arr[3]]; 302 | } 303 | } 304 | // Combine above arrays 305 | if(count($conns)) { 306 | // Local connects 307 | foreach($conns as $node) { 308 | $n = $node[0]; 309 | $curNodes[$n]['node'] = $node[0]; 310 | $curNodes[$n]['info'] = getAstInfo($fp, $node[0]); 311 | $curNodes[$n]['ip'] = $node[1]; 312 | if(isset($node[5])) { 313 | $curNodes[$n]['direction'] = $node[3]; 314 | $curNodes[$n]['elapsed'] = $node[4]; 315 | $curNodes[$n]['link'] = $node[5]; 316 | } else { 317 | $curNodes[$n]['direction'] = $node[2]; 318 | $curNodes[$n]['elapsed'] = $node[3]; 319 | if(isset($modes[$n]['mode'])) 320 | $curNodes[$n]['link'] = ($modes[$n]['mode'] === 'C') ? "Connecting" : "Established"; 321 | } 322 | $curNodes[$n]['keyed'] = 'N/A'; 323 | $curNodes[$n]['last_keyed'] = 'N/A'; 324 | $curNodes[$n]['mode'] = isset($modes[$n]) ? $modes[$n]['mode'] : 'Local Monitor'; 325 | $n++; 326 | } 327 | // Pull in keyed 328 | foreach($keyups as $node => $arr) { 329 | $curNodes[$node]['keyed'] = $arr['isKeyed'] ? 'yes' : 'no'; 330 | $curNodes[$node]['last_keyed'] = $arr['keyed']; 331 | } 332 | $curNodes[1]['node'] = 1; 333 | } else { 334 | $curNodes[1]['info'] = "NO CONNECTION"; 335 | } 336 | $curNodes[1]['cos_keyed'] = ($rxKeyed === "1") ? 1 : 0; 337 | $curNodes[1]['tx_keyed'] = ($txKeyed === "1") ? 1 : 0; 338 | // Add list of all connected nodes 339 | $curNodes[1]['lnodes'] = $lnodes; 340 | return $curNodes; 341 | } 342 | -------------------------------------------------------------------------------- /cfg/CfgView.php: -------------------------------------------------------------------------------- 1 | tableOpen($hdrCols, null, 'favs', null); 18 | $nullVal = '-'; 19 | foreach($cfgs as $id => $val) { 20 | $name = $gCfgName[$id]; 21 | $updated = $gCfgUpdated[$id] ?? null; 22 | if($updated) { 23 | $updated = getTimestamp($updated, $user->timezone_id); 24 | } else { 25 | $updated = $nullVal; 26 | } 27 | $def = $gCfgDef[$id]; 28 | if($gCfgVals[$id]) { 29 | $val = $gCfgVals[$id][$val]; 30 | $def = $gCfgVals[$id][$def]; 31 | } elseif(is_array($val)) { 32 | $val = implode(', ', $val); 33 | $def = implode(', ', $def); 34 | } 35 | if($val === $def) 36 | $val = '[Default]'; 37 | if(empty($def)) 38 | $def = '-'; 39 | $row = [$id, $name, $val, $def, $updated]; 40 | $out .= $html->tableRow($row, null); 41 | } 42 | $out .= $html->tableClose(); 43 | echo $out; 44 | } 45 | 46 | function showFiles($files, $activeFile) { 47 | global $html, $wwwroot, $asdir; 48 | $hdrCols = ['File', 'Size (Bytes)', 'Last Modified', 'Options']; 49 | $out = $html->tableOpen($hdrCols, null, 'favs', null); 50 | foreach($files as $f) { 51 | $parms = ['Submit'=>DOWNLOAD_FILE, 'file'=>$f->name]; 52 | $info = ($f->name === $activeFile) ? ' [Default]' : ''; 53 | $dl = $html->a(getScriptName(), $parms, $f->name); 54 | $parms = ['Submit'=>DELETE_FILE, 'file'=>$f->name]; 55 | $del = $html->a(getScriptName(), $parms, 'Delete'); 56 | $row = [$dl . $info, $f->size, $f->mtime, $del]; 57 | $out .= $html->tableRow($row, null); 58 | } 59 | $out .= $html->tableClose(); 60 | echo $out; 61 | } 62 | 63 | function showForms($cfg) { 64 | global $html, $user, $gCfgName, $gCfgVals; 65 | $form = new stdClass(); 66 | if($cfg !== null) { 67 | $id = $cfg->cfg_id; 68 | $val = $cfg->val; 69 | // Show Edit form 70 | $form->fieldsetLegend = EDIT_CFG; 71 | $form->submit = [EDIT_CFG, CANCEL]; 72 | if($gCfgVals[$id]) { 73 | $ctrl = ['select' => ['val', $gCfgVals[$id], $val]]; 74 | } else { 75 | if(is_array($val)) 76 | $val = implode(', ', $val); 77 | $ctrl = ['text' => ['val', $val]]; 78 | } 79 | $form->fields = [ 80 | 'Cfg Name' => ['r' => ['name', $gCfgName[$id]]], 81 | 'Value' => $ctrl]; 82 | $form->id = 'editCfgForm'; 83 | $form->hiddenFields['cfg_id'] = $id; 84 | echo htmlForm($form) . BR; 85 | } else { 86 | // Show Edit request form 87 | $list = []; 88 | foreach($gCfgName as $k => $name) 89 | $list[$k] = $name; 90 | $form->fieldsetLegend = EDIT_CFG; 91 | $form->submit = [EDIT_CFG, DEFAULT_CFG]; 92 | $form->id = 'editCfgForm'; 93 | $form->fields = ['Cfg Name' => ['select'=> ['cfg_id', $list]]]; 94 | echo htmlForm($form); 95 | } 96 | } 97 | 98 | function showFavsCopyForm($files) { 99 | global $html; 100 | $form = new stdClass(); 101 | // Show Edit form 102 | $form->fieldsetLegend = COPY_FILE; 103 | $form->submit = COPY_FILE; 104 | //$localdir = $wwwroot . '/' . $asdir . '/'; 105 | $fl = []; 106 | foreach($files as $f) 107 | $fl[$f] = $f; 108 | $dl = []; 109 | $dirs = [asDir(), asDir(false)]; 110 | foreach($dirs as $d) 111 | $dl[$d] = $d; 112 | $form->fields = [ 113 | 'File to Copy' => ['select' => ['file', $fl]], 114 | 'Destination Dir' => ['select' => ['dir', $dl]], 115 | 'Name Suffix (optional)' => ['text' => ['suffix']] 116 | ]; 117 | $form->id = 'copyFileForm'; 118 | //$form->hiddenFields['cfg_id'] = $id; 119 | echo htmlForm($form) . BR; 120 | } 121 | 122 | function showFavsUploadForm() { 123 | global $html; 124 | $form = new stdClass(); 125 | $form->fieldsetLegend = 'Upload File'; 126 | $form->submit = UPLOAD_FILE; 127 | $dl = []; 128 | $dirs = [asDir(), asDir(false)]; 129 | foreach($dirs as $d) 130 | $dl[$d] = $d; 131 | $form->fields = [ 132 | 'Favorites File' => ['f' => ['fileupload']], 133 | 'Destination Dir' => ['select' => ['dir', $dl]], 134 | ]; 135 | echo htmlForm($form) . BR; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /cfg/index.php: -------------------------------------------------------------------------------- 1 | validate(); 22 | if(!readOk()) 23 | redirect('user/'); 24 | 25 | $msg = []; 26 | $parms = getRequestParms(); 27 | // Ignore Add/Edit requests if not Admin user 28 | if(isset($parms['Submit']) && adminUser() && $parms['Submit'] !== CANCEL) { 29 | $formvars = ['cfg_id', 'val', 'file', 'dir', 'suffix', 'confirm']; 30 | $cfg = processForm($parms['Submit'], arrayToObj($parms, $formvars), $msg); 31 | } 32 | 33 | pageInit(); 34 | h1("Manage Cfgs"); 35 | $view = new CfgView(); 36 | 37 | if(!empty($msg)) { 38 | h3("Process Form Results:"); 39 | echo implode(BR, $msg) . BR; 40 | } 41 | 42 | // Show Cfgs 43 | h2("Configuration Parameters"); 44 | $view->showCfgs($gCfg); 45 | 46 | // Show Edit forms 47 | if(adminUser()) 48 | $view->showForms($cfg ?? null); 49 | 50 | if(adminUser()) { 51 | h1("Manage Favorites"); 52 | h2("View/Download/Delete Favorites Files"); 53 | chdir('..'); // NOTE: chdir back to pwd later if needed for other file system operations 54 | $activeFile = ''; 55 | $files = findFavsFiles($activeFile); 56 | $cnt = 0; 57 | $fl = []; 58 | foreach($files as $f) { 59 | $r = new stdClass(); 60 | $r->name = $f; 61 | $r->size = filesize($f); 62 | $r->mtime = getTimestamp(filemtime($f)); 63 | $fl[] = $r; 64 | $cnt++; 65 | } 66 | if($cnt) { 67 | $view->showFiles($fl, $activeFile); 68 | } 69 | 70 | if(!$cnt) { 71 | p("No Favorites files found."); 72 | } else { 73 | h2("Copy/Backup Favorites Files"); 74 | 75 | $view->showFavsCopyForm($files); 76 | 77 | p('The Favorites file select control on the main page enables easy switching between files, supporting grouping of favorites by location, type, interests, etc. A new favorites file can be created by copying and then editing an existing file, or uploading a new file. Favorites files can be stored in the AllScan web folder or in /etc/allscan/. If you have multiple AllScan instances installed (eg. for different node #s) files in /etc/allscan/ can be used by all instances.', 'w800', false); 78 | } 79 | 80 | h2("Upload Favorites File"); 81 | 82 | $view->showFavsUploadForm(); 83 | 84 | p('Favorites file names must be in the format favorites[-*].ini, ie. with an optional suffix before the .ini extension. Example valid filenames: favorites.ini, favorites-WestCoast.ini, favorites-UK.ini, favorites-nets.ini, etc.', 'w800', false); 85 | } 86 | 87 | asExit(); 88 | 89 | //-------- functions -------- 90 | function processForm($Submit, $cfg, &$msg) { 91 | global $cfgModel, $gCfg, $gCfgDef, $gCfgUpdated, $gCfgVals; 92 | 93 | if($Submit === DELETE_FILE || $Submit === CONFIRM_DELETE_FILE) { 94 | if(!isset($cfg->file)) 95 | return null; 96 | $file = trim($cfg->file); 97 | if(!validateFavsFile($file, $msg)) 98 | return null; 99 | if($Submit !== CONFIRM_DELETE_FILE) { 100 | pageInit(); 101 | echo confirmForm("Confirm Delete", "Permanently delete \"$file\"?", 102 | (array)$cfg, null, false, [CONFIRM_DELETE_FILE, 'Cancel']); 103 | exit(); 104 | } 105 | if(unlink($file)) 106 | $msg[] = ok("Deleted $file OK"); 107 | else 108 | $msg[] = error("Delete $file Failed, check directory permissions"); 109 | return; 110 | } 111 | 112 | if($Submit === COPY_FILE) { 113 | if(!isset($cfg->file) || !isset($cfg->dir)) 114 | return null; 115 | $file = trim($cfg->file); 116 | if(!validateFavsFile($file, $msg)) 117 | return null; 118 | $dir = trim($cfg->dir); 119 | if(!preg_match('/^[\/\w\-. ]+$/', $dir)) { 120 | $msg[] = error("Invalid directory name"); 121 | return null; 122 | } 123 | $suffix = $cfg->suffix ? trim($cfg->suffix, " \n\r\t\v\x00-") : ''; 124 | if($suffix && !preg_match('/^[\w\-. ]+$/', $suffix)) { 125 | $msg[] = error("Invalid characters in suffix"); 126 | return null; 127 | } 128 | //$name = basename($file, '.ini'); 129 | //$new = $dir . trim($name); 130 | $new = $dir . 'favorites'; 131 | if($suffix) 132 | $new .= '-' . $suffix; 133 | $new .= '.ini'; 134 | if(file_exists($new)) 135 | $msg[] = error("File $new already exists. Use Delete option to remove existing file before copying, or add a suffix to the name"); 136 | elseif(copy($file, $new)) 137 | $msg[] = ok("Copied $file to $new OK"); 138 | else 139 | $msg[] = error("Copy to $new Failed, check directory permissions"); 140 | return; 141 | } 142 | 143 | if($Submit === DOWNLOAD_FILE) { 144 | if(!isset($cfg->file)) 145 | return null; 146 | $file = trim($cfg->file); 147 | if(!validateFavsFile($file, $msg)) 148 | return null; 149 | $name = basename($cfg->file); 150 | outputTxtHeader($name); 151 | readfile($file); 152 | exit(); 153 | } 154 | 155 | if($Submit === UPLOAD_FILE) { 156 | if(!isset($_FILES['fileupload'])) { 157 | return null; 158 | } 159 | $f = (object)$_FILES['fileupload']; 160 | /* [name] => favorites.ini 161 | [full_path] => favorites.ini 162 | [type] => application/octet-stream 163 | [tmp_name] => /tmp/phpCdcA9I 164 | [error] => 0 165 | [size] => 3107 */ 166 | if($f->error || !$f->size) { 167 | //$msg[] = varDump($f, true); 168 | $msg[] = error("File upload error $f->error on \"$f->name\""); 169 | return null; 170 | } 171 | if(!validateFavsFile($f->name, $msg, false)) { 172 | unlink($f->tmp_name); 173 | return null; 174 | } 175 | if(!isset($cfg->dir)) { 176 | unlink($f->tmp_name); 177 | return null; 178 | } 179 | $dir = trim($cfg->dir); 180 | if(!preg_match('/^[\/\w\-. ]+$/', $dir)) { 181 | $msg[] = error("Invalid directory name"); 182 | unlink($f->tmp_name); 183 | return null; 184 | } 185 | $new = $dir . $f->name; 186 | if(file_exists($new)) 187 | $msg[] = error("File $new already exists. Use Delete option to remove existing file before uploading"); 188 | elseif(copy($f->tmp_name, $new)) 189 | $msg[] = ok("Uploaded $new OK"); 190 | else 191 | $msg[] = error("Upload $new Failed, check directory permissions"); 192 | unlink($f->tmp_name); 193 | return; 194 | } 195 | 196 | $id = $cfg->cfg_id; 197 | $val = $cfg->val ?? null; 198 | if(!array_key_exists($id, $gCfg)) { 199 | $msg[] = error('Cfg not found'); 200 | return; 201 | } 202 | if($Submit === EDIT_CFG) { 203 | if($val === null) { 204 | $cfg->val = $gCfg[$cfg->cfg_id]; 205 | return $cfg; 206 | } else { 207 | // Convert array cfgs from csv / validate enumerated cfgs 208 | if(is_array($gCfgDef[$id])) { 209 | $val = csvToArray($val); 210 | } elseif($gCfgVals[$id] !== null && !array_key_exists($val, $gCfgVals[$id])) { 211 | $msg[] = error('Invalid Cfg value'); 212 | return; 213 | } 214 | // Return now if cfg val did not change 215 | if(cfgCompare($val, $gCfg[$id])) { 216 | $msg[] = 'Cfg val unchanged'; 217 | return null; 218 | } else { 219 | //$msg[] = "cfg={$gCfg[$id]} val=$val"; 220 | } 221 | // Set Cfg 222 | $msg[] = 'Setting Cfg'; 223 | $gCfg[$id] = $val; 224 | $gCfgUpdated[$id] = time(); 225 | } 226 | } elseif($Submit === DEFAULT_CFG) { 227 | // Return now if cfg val is already the default 228 | if(cfgCompare($gCfg[$id], $gCfgDef[$id])) { 229 | $msg[] = 'Cfg val already = default val'; 230 | return null; 231 | } else { 232 | //$msg[] = "cfg={$gCfg[$id]} Defval={$gCfgDef[$id]}"; 233 | } 234 | // Set Cfg to default val 235 | $msg[] = 'Defaulting Cfg'; 236 | $gCfg[$id] = $gCfgDef[$id]; 237 | unset($gCfgUpdated[$id]); 238 | } else { 239 | return null; 240 | } 241 | $msg[] = 'Saving Cfgs'; 242 | $cfgModel->saveCfgs(); 243 | if($cfgModel->error) 244 | $msg[] = error($cfgModel->error); 245 | return null; 246 | } 247 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color:hsl(240,25%,12%); 3 | scrollbar-color:gray #444; 4 | width:100%; 5 | } 6 | ::-webkit-scrollbar { 7 | width:15px; 8 | height:15px; 9 | background-color:#444; 10 | } 11 | ::-webkit-scrollbar-thumb { 12 | background:gray; 13 | border-radius:5px; 14 | } 15 | body { 16 | margin:0px auto 7px; 17 | padding:0 5px; 18 | color:rgb(200,195,188); 19 | min-width:350px; 20 | max-width:1800px; 21 | text-align:center; 22 | } 23 | body, textarea, input, select { 24 | font:12px/1.25 Verdana,Arial,Helvetica,sans-serif; 25 | } 26 | input[type=text], input[type=number], input[type=password], select, textarea { 27 | background-color:hsl(240,50%,30%); 28 | color:#fff; 29 | margin:1px 4px; 30 | padding:2px 4px; 31 | border:2px; 32 | } 33 | input[type=text], input[type=number], input[type=password], textarea, fieldset select { 34 | width:95%; 35 | } 36 | input[type=button], input[type=submit], input::file-selector-button { 37 | color:#fff; 38 | margin:2px 1px; 39 | background-color:#444; 40 | border-radius:3px; 41 | } 42 | input[type=button]:hover, input[type=submit]:hover, input::file-selector-button:hover { 43 | background-color:#777; 44 | } 45 | input.ctrl { 46 | font-size:14px; 47 | line-height:1; 48 | vertical-align:middle; 49 | padding:1px; 50 | } 51 | input[type=number] { 52 | -moz-appearance:textfield; 53 | } 54 | ul, ol { 55 | padding:0; 56 | margin:0 0.5em 0 1.5em; 57 | } 58 | a, a:link, a:visited, .nodeNum { 59 | color:hsl(130,50%,60%); 60 | text-decoration:none; 61 | } 62 | a:active, a:hover, .nodeNum:hover { 63 | color:hsl(130,50%,80%); 64 | text-decoration:none; 65 | } 66 | a.logo { 67 | color:hsl(210,30%,70%); 68 | font-weight:bold; 69 | } 70 | form, fieldset { 71 | display:inline-block; 72 | } 73 | fieldset, #statmsg { 74 | border:solid 3px rgb(51,51,119); 75 | padding:3px; 76 | margin:3px; 77 | border-radius:10px; 78 | } 79 | fieldset legend { 80 | font-size:16px; 81 | color:hsl(180,75%,50%); 82 | } 83 | form + h2 { 84 | margin-top:3px; 85 | } 86 | header { 87 | display:inline-block; 88 | color:hsl(210,30%,70%); 89 | background-color:rgba(51,51,119,0.75); 90 | padding:3px 7px 4px; 91 | border-bottom-left-radius:10px; 92 | border-bottom-right-radius:10px; 93 | font-size:125%; 94 | } 95 | h1 { 96 | font-size:18px; 97 | } 98 | h1, a.h1 { 99 | color:hsl(270,30%,70%); 100 | font-weight:bold; 101 | } 102 | h2 { 103 | color:hsl(240,30%,70%); 104 | font-size:16px; 105 | font-style:italic; 106 | margin-top:0.5em; 107 | margin-bottom:0.4em; 108 | } 109 | h3 { 110 | font-size:14px; 111 | } 112 | h4 { 113 | font-size:14px; 114 | margin:1px 0px; 115 | color:#5a5; 116 | } 117 | h3,h4 { 118 | color:hsl(270,20%,60%); 119 | margin-top:0.75em; 120 | margin-bottom:0; 121 | } 122 | h5 { 123 | margin:1px 0px; 124 | font-size:14px; 125 | display:inline-block; 126 | color:#5a5; 127 | } 128 | table { 129 | margin:0 auto; 130 | clear:both; 131 | border-collapse:separate; 132 | border-spacing:0; 133 | border-radius:10px; 134 | overflow:hidden; 135 | } 136 | td { 137 | max-width:32em; 138 | } 139 | p { 140 | max-width:80%; 141 | margin:0.5em auto; 142 | font-size:13px; 143 | } 144 | p.w800 { 145 | max-width:800px; 146 | } 147 | ul, ol { 148 | display:inline-block; 149 | max-width:80%; 150 | margin:0.5em auto; 151 | font-size:13px; 152 | } 153 | pre { 154 | text-align:left; 155 | white-space:pre-wrap; 156 | background-color:#223; 157 | padding:0.9em; 158 | margin-left:0.6em; 159 | margin-right:0.6em; 160 | max-width:800px; 161 | } 162 | 163 | #hb img { 164 | vertical-align:text-bottom; 165 | border-radius:0; 166 | } 167 | #node { 168 | border:2px solid hsl(240,40%,60%); 169 | margin:2px; 170 | padding:0 2px; 171 | font-size:14px; 172 | width:6em; 173 | } 174 | #statmsg { 175 | display:inline-block; 176 | margin:2px; 177 | min-width:460px; 178 | max-width:750px; 179 | height:4.5em; 180 | font-size:0.9em; 181 | line-height:1.2; 182 | overflow-y:scroll; 183 | resize:both; 184 | color:#aaa; 185 | } 186 | #scanmsg { 187 | color:#999; 188 | margin:2px auto; 189 | font-size:0.9em; 190 | line-height:1.2; 191 | font-family:"Lucida Console",Courier,monospace; 192 | max-width:95%; 193 | } 194 | 195 | table tr th, table tr td { 196 | border-right:1px solid #555; 197 | border-bottom:1px solid #555; 198 | } 199 | table tr th:first-child, table tr td:first-child { 200 | border-left:1px solid #555; 201 | } 202 | table tr:first-child th { 203 | border-top:1px solid #555; 204 | } 205 | table tr:first-child th:first-child { 206 | border-top-left-radius:10px; 207 | } 208 | table tr:first-child th:last-child { 209 | border-top-right-radius:10px; 210 | } 211 | table tr:last-child td:first-child { 212 | border-bottom-left-radius:10px; 213 | } 214 | table tr:last-child td:last-child { 215 | border-bottom-right-radius:10px; 216 | } 217 | 218 | table.noborder, table.noborder tr th, table.noborder tr td { 219 | border:0; 220 | } 221 | table.favs { 222 | margin:0 auto 0.5em; 223 | } 224 | table.favs thead tr { 225 | background-color:rgba(51,51,119,0.75); 226 | } 227 | table.favs tbody tr { 228 | line-height:100%; 229 | } 230 | table.favs tbody tr:hover { 231 | background-color:rgba(51,51,119,0.67); 232 | } 233 | table.favs th, table.favs td { 234 | padding:3px; 235 | } 236 | 237 | small { 238 | font:11px/1.25 "Helvetica Narrow","Arial Narrow",Verdana,Arial,Helvetica,sans-serif; 239 | } 240 | 241 | table.grid { 242 | min-width:460px; 243 | } 244 | table.grid thead tr { 245 | background-color:rgba(51,51,119,0.75); 246 | line-height:100%; 247 | } 248 | table.grid th, table.grid td { 249 | padding:4px; 250 | } 251 | table.grid tr.cColor td { /* Link Connecting */ 252 | background-color:purple; 253 | color:yellow; 254 | } 255 | table.grid tr.rColor td, table.grid tr.rColor td a { /* Link Keyed */ 256 | background-color:maroon; 257 | color:yellow; 258 | } 259 | table.grid tr.gColor td { /* Node Idle */ 260 | background-color:hsl(150,50%,15%); 261 | } 262 | table.grid tr.tColor td { /* Node PTT */ 263 | background-color:maroon; 264 | } 265 | table.grid tr.lColor td { /* Node COS */ 266 | background-color:green; 267 | } 268 | table.grid tr.bColor td { /* Node COS+PTT */ 269 | background-color:#660; 270 | } 271 | div.twrap { 272 | display:inline-block; 273 | border:solid 2px hsl(0,0%,40%); 274 | padding:0; 275 | border-radius:12px; 276 | } 277 | label, input[type=button].small, input[type=submit].small { 278 | font-size:11px; 279 | } 280 | a.menu { 281 | display:inline-block; 282 | padding:3px; 283 | text-decoration:none; 284 | color:#fff; 285 | background-color:rgb(51,51,119); 286 | } 287 | a.menu:link, a.menu:visited { 288 | color:#fff; 289 | } 290 | a.menu:hover { 291 | background-color:hsl(120,50%,33%); 292 | } 293 | 294 | .ok { color:#7f7; } 295 | .error { color:#f77;} 296 | .left { text-align:left; } 297 | .right { text-align:right; } 298 | .center { text-align:center; vertical-align:middle; } 299 | .floatleft { position:relative; float:left; } 300 | .floatright { position:relative; float:right; } 301 | .gray { color:#888; } 302 | .green { color:#7e7; } 303 | .ib { display:inline-block; } 304 | .m5 { margin:5px; } 305 | .nodeNum { cursor:pointer; } 306 | .w600 { max-width:600px; } 307 | 308 | .button1 { 309 | font-size:12px; 310 | background-color:#448; 311 | color:#fff; 312 | } 313 | .greenborder { 314 | border:solid 3px rgb(39,144,39); 315 | border-radius:15px; 316 | } 317 | 318 | pre.error { 319 | font-weight:bold; 320 | font-size:15px; 321 | } 322 | a.play::after { 323 | color:#0f0; 324 | font-size:11px; 325 | content:"\00A0\25B6"; 326 | } 327 | a.dl::after { 328 | color:#0f0; 329 | font-size:12px; 330 | content:"\200A\25BC"; 331 | } 332 | -------------------------------------------------------------------------------- /docs/extensions.conf: -------------------------------------------------------------------------------- 1 | [my-ip] 2 | ;exten => s,1,Wait(1) 3 | ; same => n,SayAlpha(${CURL(http://myip.vg)}) 4 | ; same => n,Hangup 5 | 6 | ; *690: Say LAN IP Address 7 | exten => 0,1,Wait(2) 8 | exten => 0,n,AGI(getip.sh) 9 | exten => 0,n,SayAlpha(${result}) 10 | exten => 0,n,Hangup 11 | 12 | ; *691: Say WAN IP Address 13 | exten => 1,1,Wait(2) 14 | exten => 1,n,Set(result=${CURL(https://api.ipify.org)}) 15 | exten => 1,n,SayAlpha(${result}) 16 | exten => 1,n,Hangup 17 | 18 | ; *692: Turn Off Wi-Fi 19 | exten => 2,1,AGI(wifidown.sh) 20 | exten => 2,n,Wait(2) 21 | exten => 2,n,Playback(/usr/share/asterisk/agi-bin/wifidisabled) 22 | exten => 2,n,Hangup 23 | 24 | ; *693: Turn On Wi-Fi 25 | exten => 3,1,AGI(wifiup.sh) 26 | exten => 3,n,Wait(2) 27 | exten => 3,n,Playback(/usr/share/asterisk/agi-bin/wifienabled) 28 | exten => 3,n,Wait(1) 29 | exten => 3,n,Hangup 30 | 31 | ; *694: Reboot Node 32 | exten => 4,1,Wait(2) 33 | exten => 4,n,Playback(/usr/share/asterisk/agi-bin/reboot) 34 | exten => 4,n,Wait(1) 35 | exten => 4,n,AGI(reboot.sh) 36 | exten => 4,n,Hangup 37 | 38 | ; *695: Power Off Node 39 | exten => 5,1,Wait(2) 40 | exten => 5,n,Playback(/usr/share/asterisk/agi-bin/poweroff) 41 | exten => 5,n,Wait(1) 42 | exten => 5,n,AGI(poweroff.sh) 43 | exten => 5,n,Hangup 44 | 45 | ; *698: Announce LAN IP Address & mDNS URL using asl-tts 46 | exten => 8,1,AGI(sayip.sh,${NODE}) 47 | exten => 8,n,Hangup 48 | 49 | ; *699: Play test file 50 | exten => 9,1,Wait(2) 51 | exten => 9,n,Playback(/usr/share/asterisk/agi-bin/test) 52 | exten => 9,n,Hangup 53 | -------------------------------------------------------------------------------- /docs/favorites.ini.sample: -------------------------------------------------------------------------------- 1 | ; The %node% template will be replaced with your node number at run time. 2 | ; Put favorites in the [general] section that will show for all nodes. 3 | ; 4 | [general] 5 | ; 6 | 7 | label[] = " === SELECT ===" 8 | cmd[] = " " 9 | 10 | label[] = "AA6DP, Catalina, CA 51597" 11 | cmd[] = "rpt cmd %node% ilink 3 51597" 12 | 13 | label[] = "AB9S HUB1-ASL2, Chicago,IL 47487" 14 | cmd[] = "rpt cmd %node% ilink 3 47487" 15 | 16 | label[] = "AG4VA SOTA Hub, Cloud, VA 46969" 17 | cmd[] = "rpt cmd %node% ilink 3 46969" 18 | 19 | label[] = "East Coast Reflector 27339" 20 | cmd[] = "rpt cmd %node% ilink 3 27339" 21 | 22 | label[] = "GB3CR North Wales UK 512511" 23 | cmd[] = "rpt cmd %node% ilink 3 512511" 24 | 25 | label[] = "GB3EG 430.9125, wigan, greater manchester 54318" 26 | cmd[] = "rpt cmd %node% ilink 3 54318" 27 | 28 | label[] = "GB3PI Barkway, Hertfordshire, UK 50683" 29 | cmd[] = "rpt cmd %node% ilink 3 50683" 30 | 31 | label[] = "GB3ZB 430.825, Dundry, Bristol 2514" 32 | cmd[] = "rpt cmd %node% ilink 3 2514" 33 | 34 | label[] = "GB7SJ 433.175 Northwich Cheshire UK 41360" 35 | cmd[] = "rpt cmd %node% ilink 3 41360" 36 | 37 | label[] = "K4JDR 441.725+Backbone HUB, Raleigh, NC 42235" 38 | cmd[] = "rpt cmd %node% ilink 3 42235" 39 | 40 | label[] = "K6RRR San Diego Hangout 28404" 41 | cmd[] = "rpt cmd %node% ilink 3 28404" 42 | 43 | label[] = "K6TZ SBARC Hub, Santa Barbara, CA 43763" 44 | cmd[] = "rpt cmd %node% ilink 3 43763" 45 | 46 | label[] = "K9IIK Schaumburg, IL 27833" 47 | cmd[] = "rpt cmd %node% ilink 3 27833" 48 | 49 | label[] = "K9SA 443.250, Round Lake, IL 59246" 50 | cmd[] = "rpt cmd %node% ilink 3 59246" 51 | 52 | label[] = "K9TAY CFMC 144 - 146.760, Chicago, IL 555491" 53 | cmd[] = "rpt cmd %node% ilink 3 555491" 54 | 55 | label[] = "K9TAY CFMC 440 - 443.750, Chicago, IL 555492" 56 | cmd[] = "rpt cmd %node% ilink 3 555492" 57 | 58 | label[] = "KR8T 443.800+, Grand Rapids, MI 47243" 59 | cmd[] = "rpt cmd %node% ilink 3 47243" 60 | 61 | label[] = "KR9RK 442.000, Racine,Wisconsin 511681" 62 | cmd[] = "rpt cmd %node% ilink 3 511681" 63 | 64 | label[] = "M0HOY HUBNet, Manchester, UK 51288" 65 | cmd[] = "rpt cmd %node% ilink 3 51288" 66 | 67 | label[] = "M0JKT FreeSTAR UK HUB 1 2196" 68 | cmd[] = "rpt cmd %node% ilink 3 2196" 69 | 70 | label[] = "N0ECT HUB/Server, Glenwood Springs, Colorado 45479" 71 | cmd[] = "rpt cmd %node% ilink 3 45479" 72 | 73 | label[] = "N4LMC W4GTA 145.350, Lookout Mtn, GA - EL W4EDP-R 46331" 74 | cmd[] = "rpt cmd %node% ilink 3 46331" 75 | 76 | label[] = "N6LXX MAIN HUB 29833" 77 | cmd[] = "rpt cmd %node% ilink 3 29883" 78 | 79 | label[] = "N8UKF 443.200 Fusion, Muskegon Michigan 54576" 80 | cmd[] = "rpt cmd %node% ilink 3 54576" 81 | 82 | label[] = "N9GMT FM38 Network Hub, Milwaukee, WI 2495" 83 | cmd[] = "rpt cmd %node% ilink 3 2495" 84 | 85 | label[] = "NWAG NW AllStar Group, Morecambe, UK 53573" 86 | cmd[] = "rpt cmd %node% ilink 3 53573" 87 | 88 | label[] = "Parrot+ enhanced parrot, Dallas, TX 55553" 89 | cmd[] = "rpt cmd %node% ilink 3 55553" 90 | 91 | label[] = "W2JLD Hub 1, Rochester, NY 47620" 92 | cmd[] = "rpt cmd %node% ilink 3 47620" 93 | 94 | label[] = "W3QV/R 147.03 PMRC, Roxborough Philadelphia PA 47970" 95 | cmd[] = "rpt cmd %node% ilink 3 47970" 96 | 97 | label[] = "W4KEV/ R 145.370 Knoxville, TN 50015" 98 | cmd[] = "rpt cmd %node% ilink 3 50015" 99 | 100 | label[] = "W6EK 145.430, Auburn, CA 51018" 101 | cmd[] = "rpt cmd %node% ilink 3 51018" 102 | 103 | label[] = "W7HEN HUB, Henderson, NV 44045" 104 | cmd[] = "rpt cmd %node% ilink 3 44045" 105 | 106 | label[] = "W7YRC 146.880, Prescott, AZ 603231" 107 | cmd[] = "rpt cmd %node% ilink 3 603231" 108 | 109 | label[] = "W8IRA 147.160+, Grand Rapids, MI 472440" 110 | cmd[] = "rpt cmd %node% ilink 3 472440" 111 | 112 | label[] = "W8MW SOUTHCARS HUB, US 27404" 113 | cmd[] = "rpt cmd %node% ilink 3 27404" 114 | 115 | label[] = "W9BMK P25 Bridge Astro DIU, Chicago, IL 598730" 116 | cmd[] = "rpt cmd %node% ilink 3 598730" 117 | 118 | label[] = "W9HHX 145.270-, Milwaukee, WI 42742" 119 | cmd[] = "rpt cmd %node% ilink 3 42742" 120 | 121 | label[] = "W9SBE Lkpt Hub, Lockport, IL 43628" 122 | cmd[] = "rpt cmd %node% ilink 3 43628" 123 | 124 | label[] = "WA5AIR HUB#1, SouthCoastReflector.com [USA] 48752" 125 | cmd[] = "rpt cmd %node% ilink 3 48752" 126 | 127 | label[] = "WB9YPA/R 223.980 (-1.6MHz), South Bend, Indiana 51560" 128 | cmd[] = "rpt cmd %node% ilink 3 51560" 129 | 130 | label[] = "WM9W Chicago 458800" 131 | cmd[] = "rpt cmd %node% ilink 3 458800" 132 | 133 | label[] = "WW7PSR 146.960 Seattle, WA 2462" 134 | cmd[] = "rpt cmd %node% ilink 3 2462" 135 | 136 | ; 137 | ; Put favorites for specific nodes here. 138 | ; 139 | ;[1999] 140 | ; 141 | -------------------------------------------------------------------------------- /docs/global.inc.sample: -------------------------------------------------------------------------------- 1 | PERMISSION_READ_ONLY, 19 | favsIniLoc => ['favorites.ini', '../supermon/favorites.ini', '/etc/allscan/favorites.ini'], 20 | call => '', 21 | location => '', 22 | title => '', 23 | autodisc_def => 1 24 | ]; 25 | 26 | $gCfgName = [ 27 | publicPermission => 'Public Permission', 28 | favsIniLoc => 'Favorites.ini Locations', 29 | call => 'Call Sign', 30 | location => 'Location', 31 | title => 'Node Title', 32 | autodisc_def => 'DiscBeforeConn Default' 33 | ]; 34 | 35 | $publicPermissionVals = [ 36 | PERMISSION_NONE => 'None (No Access)', 37 | PERMISSION_READ_ONLY => 'Read Only', 38 | PERMISSION_READ_MODIFY => 'Read/Modify', 39 | PERMISSION_FULL => 'Full']; 40 | 41 | $checkboxVals = [0=>'Off', 1=>'On']; 42 | 43 | // Value definition arrays for enumerated cfgs. Specify null for plain text/numeric cfgs 44 | $gCfgVals = [ 45 | publicPermission => $publicPermissionVals, 46 | favsIniLoc => null, 47 | call => null, 48 | location => null, 49 | title => null, 50 | autodisc_def => $checkboxVals 51 | ]; 52 | 53 | // Global Cfgs structure 54 | $gCfg = $gCfgDef; 55 | // Last update time of each gCfg (unix tstamp) 56 | $gCfgUpdated = []; 57 | 58 | // Below functions used to enable/disable site functions based on user and global permission settings 59 | // CfgModel and UserModel classes must be instantiated before below are called. 60 | // If readOk() returns false user is not allowed access to any pages or data. 61 | function readOk() { 62 | global $user, $gCfg; 63 | return (isset($gCfg[publicPermission]) && $gCfg[publicPermission] >= PERMISSION_READ_ONLY) 64 | || (isset($user) && userPermission() >= PERMISSION_READ_ONLY); 65 | } 66 | 67 | function modifyOk() { 68 | global $user, $gCfg; 69 | return (isset($gCfg[publicPermission]) && $gCfg[publicPermission] >= PERMISSION_READ_MODIFY) 70 | || (isset($user) && userPermission() >= PERMISSION_READ_MODIFY); 71 | } 72 | 73 | function writeOk() { 74 | global $user, $gCfg; 75 | return (isset($gCfg[publicPermission]) && $gCfg[publicPermission] >= PERMISSION_FULL) 76 | || (isset($user) && userPermission() >= PERMISSION_FULL); 77 | } 78 | 79 | function adminUser() { 80 | global $user; 81 | return (isset($user) && userPermission() >= PERMISSION_ADMIN); 82 | } 83 | function superUser() { 84 | global $user; 85 | return (isset($user) && userPermission() >= PERMISSION_SUPERUSER); 86 | } 87 | 88 | function cfgCompare($a, $b) { 89 | if(is_numeric($a) && is_numeric($b)) 90 | return ($a == $b); 91 | return ($a === $b); 92 | } 93 | 94 | class CfgModel { 95 | const TABLENAME = 'cfg'; 96 | public $db; 97 | public $error; 98 | 99 | function __construct($db) { 100 | global $msg, $asdbfile; 101 | $this->db = $db; 102 | // Read global cfgs 103 | $this->readCfgs(); 104 | if($this->error) { 105 | pageInit(); 106 | if(!empty($msg) && is_array($msg)) 107 | echo implode(BR, $msg) . BR; 108 | msg("Cfg Init failed. $asdbfile may be corrupted. Try copying .bak over .db if exists or delete .db file."); 109 | asExit($this->error); 110 | } 111 | } 112 | 113 | // Read global cfgs from DB into $gCfg 114 | function readCfgs() { 115 | global $gCfg, $gCfgDef, $gCfgUpdated; 116 | $ids = implode(',', array_keys($gCfg)); 117 | $where = "cfg_id IN($ids)"; 118 | $cfgs = $this->getCfgs($where); 119 | if(empty($cfgs)) 120 | return; 121 | foreach($cfgs as $c) { 122 | $k = $c->cfg_id; 123 | $gCfg[$k] = is_array($gCfgDef[$k]) ? explode(',', $c->val) : $c->val; 124 | //msg("Cfg $k val=" . $gCfg[$k]); 125 | $gCfgUpdated[$k] = $c->updated; 126 | } 127 | } 128 | 129 | // Save global cfgs. Caller will have updated $gCfg. Loop through cfgs, compare vals to DB & Def Vals 130 | function saveCfgs() { 131 | global $gCfg, $gCfgDef, $gCfgUpdated; 132 | $ids = array_keys($gCfg); 133 | $where = "cfg_id IN(" . arrayToCsv($ids). ")"; 134 | $cfgs = $this->getCfgs($where); 135 | foreach($ids as $k) { 136 | // If val=DBval nothing to be done 137 | $val = is_array($gCfgDef[$k]) ? arrayToCsv($gCfg[$k]) : $gCfg[$k]; 138 | if(isset($cfgs[$k])) { 139 | $cVal = $cfgs[$k]->val; 140 | $dbVal = is_array($gCfgDef[$k]) ? arrayToCsv($cVal) : $cVal; 141 | } else { 142 | $dbVal = null; 143 | } 144 | if(cfgCompare($val, $dbVal)) { 145 | //msg("Cfg $k val=DBval"); 146 | continue; 147 | } 148 | // If val=DefVal delete from DB, else write to DB 149 | $defVal = is_array($gCfgDef[$k]) ? arrayToCsv($gCfgDef[$k]) : $gCfgDef[$k]; 150 | if(cfgCompare($val, $defVal)) { 151 | //msg("Cfg $k val=defVal"); 152 | if($dbVal !== null) { 153 | //msg("Deleting Cfg $k"); 154 | $this->delete($k); 155 | unset($gCfgUpdated[$k]); 156 | } 157 | } else { 158 | // Add if not in DB / Update otherwise 159 | $c = (object)['cfg_id'=>$k, 'val'=>$val, 'updated'=>$gCfgUpdated[$k]]; 160 | //msg("Cfg $k val!=defVal, updating"); 161 | $this->update($c, !isset($cfgs[$k])); 162 | } 163 | } 164 | } 165 | 166 | private function getCfgs($where=null, $orderBy=null) { 167 | $cfgs = $this->db->getRecords(self::TABLENAME, $where, $orderBy); 168 | $this->checkDbError(__METHOD__); 169 | if(!_count($cfgs)) 170 | return null; 171 | // Index by ID 172 | $a = []; 173 | foreach($cfgs as $c) { 174 | // Validate update time 175 | if($c->updated < 1672964000 || !is_numeric($c->updated)) 176 | $c->updated = 0; 177 | $a[$c->cfg_id] = $c; 178 | } 179 | return $a; 180 | } 181 | private function getCfg($id) { 182 | if(!validDbID($id)) 183 | return null; 184 | $where = "cfg_id='$id'"; 185 | $cfgs = $this->getCfgs($where); 186 | return empty($cfgs) ? null : $cfgs[$id]; 187 | } 188 | private function add($c) { 189 | return $this->update($c, true); 190 | } 191 | private function update($c, $add=false) { 192 | if(!$add && !validDbID($c->cfg_id)) 193 | return null; 194 | if(!$this->validateVal($c->val)) 195 | return null; 196 | $cols = ['cfg_id', 'val', 'updated']; 197 | $vals = [$c->cfg_id, $c->val, time()]; 198 | if($add) { 199 | $retval = $this->db->insertRow(self::TABLENAME, $cols, $vals); 200 | } else { 201 | $retval = $this->db->updateRow(self::TABLENAME, $cols, $vals, "cfg_id=$c->cfg_id"); 202 | } 203 | $this->checkDbError(__METHOD__); 204 | return $retval; 205 | } 206 | private function delete($id) { 207 | if(!validDbID($id)) 208 | return null; 209 | $retval = $this->db->deleteRows(self::TABLENAME, "cfg_id=$id"); 210 | $this->checkDbError(__METHOD__); 211 | return $retval; 212 | } 213 | 214 | function getCount($where=null) { 215 | $retval = $this->db->getRecordCount(self::TABLENAME, $where); 216 | $this->checkDbError(__METHOD__); 217 | return $retval; 218 | } 219 | 220 | function validateVal($c) { 221 | if(strlen($c) > 65535) { 222 | $this->error = 'Invalid Cfg Val. Must be <= 64K chars'; 223 | return false; 224 | } 225 | return true; 226 | } 227 | 228 | // Do not call below prior to htmlInit(), global.inc include may cause whitespace to be output 229 | function checkGlobalInc() { 230 | global $gCfg, $subdir, $userModel; 231 | if($gCfg[call] && $gCfg[location] || !isset($userModel)) 232 | return true; 233 | // If Call and Location cfgs not set try importing from ../supermon/global.inc 234 | $loc = '../supermon/global.inc'; 235 | if($subdir) 236 | $loc = "../$loc"; 237 | if(strpos($subdir, '/')) 238 | $loc = "../$loc"; 239 | if(file_exists($loc)) { 240 | include($loc); 241 | if(!$CALL || !$LOCATION || strlen($CALL) > 9 || 242 | !$userModel->validateName($CALL) || !$userModel->validateLocation($LOCATION)) { 243 | unset($userModel->error); 244 | return false; 245 | } 246 | $gCfg[call] = $CALL; 247 | $gCfg[location] = $LOCATION; 248 | if($TITLE2) 249 | $gCfg[title] = $TITLE2; 250 | $this->saveCfgs(); 251 | return true; 252 | } 253 | return false; 254 | } 255 | 256 | private function checkDbError($method, $extraTxt='') { 257 | if(isset($this->db->error)) { 258 | if($extraTxt !== '') 259 | $extraTxt = "($extraTxt)"; 260 | $this->error = $method . $extraTxt . ': ' . $this->db->error; 261 | unset($this->db->error); 262 | return true; 263 | } 264 | return false; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /include/DB.php: -------------------------------------------------------------------------------- 1 | dbName = $dbName; 15 | } 16 | 17 | // init() is called automatically the first time a query is done 18 | function init() { 19 | if(!$this->dataSrc) { 20 | $this->dataSrc = new DataSource(); 21 | if($this->dataSrc->connectToDb($this->dbName)) 22 | $this->connected = true; 23 | else 24 | $this->error = $this->dataSrc->error; 25 | } 26 | return $this->connected; 27 | } 28 | 29 | function getRecords($table, $where=null, $orderBy=null, $limit=null, $select='*', $join=null, $groupBy=null) { 30 | if(!$this->init()) 31 | return null; 32 | $query = "SELECT $select FROM `$table`"; 33 | if($join) 34 | $query .= " $join"; 35 | if($where) 36 | $query .= " WHERE $where"; 37 | if($groupBy) 38 | $query .= " GROUP BY $groupBy"; 39 | if($orderBy) 40 | $query .= " ORDER BY $orderBy"; 41 | if($limit) 42 | $query .= " LIMIT $limit"; 43 | $result = $this->dataSrc->getRecordSet($query); 44 | if(!$result) { 45 | $this->error = $this->dataSrc->error; 46 | return null; 47 | } 48 | $rows = []; 49 | while(($row = $this->fetchObject($result))) { 50 | $rows[] = $row; 51 | } 52 | return $rows; 53 | } 54 | function fetchObject($res) { 55 | $row = $res->fetchArray(SQLITE3_ASSOC); 56 | return empty($row) ? null : (object)$row; 57 | } 58 | function getRecord($table, $where=null, $orderBy=null, $select='*', $join=null) { 59 | $row = $this->getRecords($table, $where, $orderBy, 1, $select, $join); 60 | if(empty($row)) 61 | return null; 62 | return $row[0]; 63 | } 64 | 65 | function getRecordCount($table, $where=null) { 66 | if(!$this->init()) 67 | return null; 68 | $query = "SELECT COUNT(*) AS cnt FROM `$table`"; 69 | if($where) 70 | $query .= " WHERE $where"; 71 | $result = $this->dataSrc->getRecordSet($query); 72 | if(!$result) { 73 | $this->error = $this->dataSrc->error; 74 | return null; 75 | } 76 | $row = $this->fetchObject($result); 77 | if(!$row) 78 | return null; 79 | return $row->cnt; 80 | } 81 | function getMaxFieldVal($table, $field, $where=null) { 82 | if(!$this->init()) 83 | return null; 84 | $query = "SELECT MAX(`$field`) AS max FROM `$table`"; 85 | if($where) 86 | $query .= " WHERE $where"; 87 | $result = $this->dataSrc->getRecordSet($query); 88 | if(!$result) { 89 | $this->error = $this->dataSrc->error; 90 | return null; 91 | } 92 | $row = $this->fetchObject($result); 93 | if(!$row) 94 | return null; 95 | return $row->max; 96 | } 97 | function getDistinctFieldList($table, $field, $where=null, $orderBy=null) { 98 | $select = "DISTINCT `$field`"; 99 | if($orderBy === null) 100 | $orderBy = "`$field` ASC"; 101 | $rows = $this->getRecords($table, $where, $orderBy, null, $select); 102 | if($rows === null) 103 | return null; 104 | $list = []; 105 | foreach($rows as $li) { 106 | $list[$li->$field] = $li->$field; 107 | } 108 | return $list; 109 | } 110 | 111 | function exec($sql) { 112 | if(!$this->init()) 113 | return null; 114 | if(!$this->dataSrc->exec($sql)) { 115 | $this->error = $this->dataSrc->error; 116 | return false; 117 | } 118 | return true; 119 | } 120 | // Returns id of the new object or null on error 121 | // (Check retval with '=== null' in case a new table's first id is 0) 122 | function insertRow($table, $cols, $vals) { 123 | if(!$this->init()) 124 | return null; 125 | $vals = $this->dataSrc->escapeArray($vals); 126 | $cols = "`" . implode("`, `", $cols) . "`"; 127 | $vals = "'" . implode("', '", $vals) . "'"; 128 | $vals = $this->unquoteSqlFunctions($vals); 129 | $query = "INSERT into `$table` ($cols) values ($vals)"; 130 | if(!$this->dataSrc->getRecordSet($query)) { 131 | $this->error = $this->dataSrc->error; 132 | return null; 133 | } 134 | return $this->getLastInsertId(); 135 | } 136 | function updateRow($table, $cols, $vals, $where) { 137 | if(!$this->init()) 138 | return null; 139 | $vals = $this->dataSrc->escapeArray($vals); 140 | for($i=0; $i < count($cols); $i++) 141 | $parms[] = "`{$cols[$i]}`='{$vals[$i]}'"; 142 | $parms = implode(", ", $parms); 143 | $parms = $this->unquoteSqlFunctions($parms); 144 | $query = "UPDATE `$table` SET $parms WHERE $where"; 145 | if(!$this->dataSrc->getRecordSet($query)) { 146 | $this->error = $this->dataSrc->error; 147 | return null; 148 | } 149 | return $this->dataSrc->getRowsAffected(); 150 | } 151 | function deleteRows($table, $where) { 152 | if(!$this->init()) 153 | return null; 154 | $query = "DELETE FROM `$table` WHERE $where"; 155 | if(!$this->dataSrc->getRecordSet($query)) { 156 | $this->error = $this->dataSrc->error; 157 | return null; 158 | } 159 | return $this->dataSrc->getRowsAffected(); 160 | } 161 | function getRecordSet($sql) { 162 | if(!$this->init()) 163 | return null; 164 | $result = $this->dataSrc->getRecordSet($sql); 165 | if($this->dataSrc->error) { 166 | $this->error = $this->dataSrc->error; 167 | return null; 168 | } 169 | $rows = []; 170 | while(($row = $this->fetchObject($result))) { 171 | $rows[] = $row; 172 | } 173 | return $rows; 174 | } 175 | 176 | function getTableList($colName=null) { 177 | $rows = $this->getRecordSet("SELECT name FROM sqlite_master WHERE type='table'"); 178 | if($this->error) 179 | return null; 180 | $a = []; 181 | foreach($rows as $r) { 182 | // If colName set, check if table contains specified col and return only those tables 183 | if($colName) { 184 | $colList = $this->getColList($r->name); 185 | if($this->error) 186 | return null; 187 | if(!in_array($colName, $colList)) 188 | continue; 189 | } 190 | $a[] = $r->name; 191 | } 192 | return $a; 193 | } 194 | function getColList($table) { 195 | // Below fails on old SQLite versions 196 | //$rows = $this->getRecordSet("SELECT name FROM PRAGMA_TABLE_INFO('$table')"); 197 | $rows = $this->getRecordSet("PRAGMA table_info('$table')"); 198 | if($this->error) 199 | return null; 200 | $a = []; 201 | foreach($rows as $r) 202 | $a[] = $r->name; 203 | return $a; 204 | } 205 | 206 | function getValCount($table, $col, $val) { 207 | $where = "`$col`='$val'"; 208 | $select = "COUNT(*)"; 209 | $res = $this->getRecord($table, $where, $select); 210 | if($this->error) 211 | return null; 212 | return $res; 213 | } 214 | 215 | /* function getLastQueryCount() { 216 | if(!$this->init()) 217 | return null; 218 | return $this->dataSrc->getLastQueryCount(); 219 | } */ 220 | function getRowsAffected() { 221 | if(!$this->init()) 222 | return null; 223 | return $this->dataSrc->getRowsAffected(); 224 | } 225 | function getLastInsertId() { 226 | if(!$this->init()) 227 | return null; 228 | return $this->dataSrc->getLastInsertId(); 229 | } 230 | 231 | function unquoteSqlFunctions($vals) { 232 | $search = array("'NOW()'", "'NULL'"); 233 | $replace = array('NOW()', 'NULL'); 234 | $vals = str_replace($search, $replace, $vals); 235 | return $vals; 236 | } 237 | 238 | function close() { 239 | if($this->connected) { 240 | $this->dataSrc->closeDb(); 241 | $this->connected = false; 242 | } 243 | if($this->dataSrc) 244 | unset($this->dataSrc); 245 | } 246 | 247 | function __destruct() { 248 | $this->close(); 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /include/DataSource.php: -------------------------------------------------------------------------------- 1 | dbName = $dbName; 14 | $this->db = new SQLite3($dbName); 15 | if(!$this->db) { 16 | $this->setError('Connect'); 17 | return false; 18 | } 19 | // Wait up to 2 secs for DB file to be unlocked if in use by another client 20 | if(!$this->db->busyTimeout(2000)) { 21 | $this->setError('setBusyTimeout'); 22 | return false; 23 | } 24 | return true; 25 | } 26 | function closeDb() { 27 | if(isset($this->db)) { 28 | $this->db->close(); 29 | unset($this->db); 30 | $this->lastQueryCount = 0; 31 | } 32 | } 33 | function getRecordSet($sql) { 34 | if(!isset($this->db)) 35 | return null; 36 | $recordSet = $this->db->query($sql); 37 | if(!$recordSet) { 38 | $this->setError("Query ($sql)"); 39 | $this->lastQueryCount = 0; 40 | return null; 41 | } 42 | if(preg_match('/^(SELECT|SHOW|EXPLAIN)/', $sql, $matches) == 1) { 43 | // Apparently there's no way to get the returned row count from SQLite3 44 | // other than loop through results or do separate COUNT query 45 | //$this->lastQueryCount = $recordSet->num_rows; 46 | } else { 47 | $this->lastQueryCount = $this->db->changes(); 48 | } 49 | return $recordSet; 50 | } 51 | function exec($sql) { 52 | if(!isset($this->db)) 53 | return null; 54 | $res = $this->db->exec($sql); 55 | if(!$res) { 56 | $this->setError("Exec ($sql)"); 57 | $this->lastQueryCount = 0; 58 | return false; 59 | } 60 | $this->lastQueryCount = $this->db->changes(); 61 | return true; 62 | } 63 | /* function getLastQueryCount() { 64 | return $this->lastQueryCount; 65 | } */ 66 | function getRowsAffected() { 67 | return $this->lastQueryCount; 68 | } 69 | function getLastInsertId() { 70 | return $this->db->lastInsertRowID(); 71 | } 72 | 73 | function escapeString($string) { 74 | $escaped = $this->db->escapeString($string); 75 | if(!$escaped) 76 | return $string; 77 | return $escaped; 78 | } 79 | function escapeArray($arr) { 80 | foreach($arr as &$string) 81 | $string = $this->escapeString($string); 82 | return $arr; 83 | } 84 | 85 | private function setError($errTypeStr) { 86 | $this->error = "Error($errTypeStr) errno " . $this->db->lastErrorCode() . ", desc " . $this->db->lastErrorMsg(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /include/Html.php: -------------------------------------------------------------------------------- 1 | \n"); 6 | define('NBSP', ' '); 7 | define('EMSP', ' '); 8 | define('ENSP', ' '); 9 | function nbsp($count=1) { $s=''; for($n=0;$n<$count;$n++) $s.= NBSP; return $s; } 10 | function htmlspecial($txt) { // Does not escape quotes. Do not use for attributes 11 | return htmlspecialchars($txt, ENT_NOQUOTES | ENT_HTML5 | ENT_IGNORE, 'UTF-8', false); 12 | } 13 | function htmlattr($txt) { // Escape an attribute (no double quotes) 14 | return htmlspecialchars($txt, ENT_COMPAT | ENT_HTML5 | ENT_IGNORE, 'UTF-8', false); 15 | } 16 | 17 | class Html 18 | { 19 | var $checkBoxId=1; 20 | function tag($tag, $txt, $class=null, $escapeTxt=false, $style=null, $id=null) { 21 | $out = "<$tag"; 22 | if($id) 23 | $out .= " id=\"$id\""; 24 | if($class) 25 | $out .= " class=\"$class\""; 26 | if($style) 27 | $out .= " style=\"$style\""; 28 | $out .= '>'; 29 | $out .= ($escapeTxt ? htmlspecial($txt) : $txt); 30 | $out .= ""; 31 | return $out; 32 | } 33 | function span($txt, $class=null, $escapeTxt=false, $style=null) { 34 | return $this->tag('span', $txt, $class, $escapeTxt, $style); 35 | } 36 | function h($num, $txt, $class=null, $escapeTxt=true) { 37 | return $this->tag("h$num", $txt, $class, $escapeTxt) . NL; 38 | } 39 | function pre($txt, $class=null, $escapeTxt=false, $style=null) { 40 | return $this->tag('pre', $txt, $class, $escapeTxt, $style) . NL; 41 | } 42 | function div($txt, $class=null, $id=null, $style=null) { 43 | return $this->tag('div', $txt, $class, false, $style, $id) . NL; 44 | } 45 | function header($txt, $class=null) { 46 | return $this->tag('header', $txt, $class) . NL; 47 | } 48 | function article($txt, $class=null) { 49 | return $this->tag('article', $txt, $class) . NL; 50 | } 51 | function p($txt, $class=null, $escapeTxt=true, $id=null) { 52 | return $this->tag('p', $txt, $class, $escapeTxt, null, $id) . NL; 53 | } 54 | function a($url, $parms=null, $title=null, $class=null, $target=null, $escapeTitle=true, $onclick=null, $style=null) { 55 | $props = []; 56 | $props[] = "$title"; 75 | return $out; 76 | } 77 | // build a url from base url and parms array, format for JS ('&' not escaped) 78 | function buildUrl($url, $parms) { 79 | if(isset($url) && strlen($url)) { 80 | if(is_array($parms)) 81 | $url .= "?". http_build_query($parms); 82 | } 83 | return $url; 84 | } 85 | function br($num=1) { 86 | for($i=0; $i < $num; $i++) 87 | $out .= BR; 88 | return $out; 89 | } 90 | function nbsp($num=1) { 91 | for($i=0; $i < $num; $i++) 92 | $out .= NBSP; 93 | return $out; 94 | } 95 | function img($url, $alt='', $class=null, $width=null, $height=null) { 96 | $out = "\"$alt\"";$text\n"; 112 | return $out; 113 | } 114 | // FORMS 115 | function formOpen($scriptName=null, $method='post', $id=null, $class=null, $target=null) { 116 | if(!$scriptName) 117 | $scriptName = getScriptName(); 118 | if($id) 119 | $id = " id=\"$id\""; 120 | if($class) 121 | $class = " class=\"$class\""; 122 | if($target) 123 | $target = " target=\"$target\""; 124 | return "\n"; 126 | } 127 | function textField($name, $label, $len=null, $val=null, $class=null, $readonly=null) { 128 | $out = $label ? $this->getLabelHtml($label, "_$name") : ''; 129 | $out .= ' $size) 147 | $size = strlen($val); 148 | $out = $label ? $this->getLabelHtml($label, "_$name") : ''; 149 | $out .= "\n"; 159 | return $out; 160 | } 161 | function checkbox($name, $label, $isChecked=false, $class=null) { 162 | global $checkBoxId; 163 | $id = 'cb' . $checkBoxId++; 164 | $out = $label ? $this->getLabelHtml($label, $id) : ''; 165 | $out .= "getLabelHtml($label, "_$name") : ''; 175 | $out .= "\n"; 189 | } 190 | function select($name, $label, $list, $selected=null, $class=null, $escapeTxt=true, $submitOnChange=false) { 191 | $out = $label ? $this->getLabelHtml($label, "_$name") : ''; 192 | $out .= '
\n"; 253 | if(strlen($label)) 254 | $out = "\n" . $out; 255 | return $out; 256 | } 257 | function dateRange($date0, $date1) { 258 | $out = $this->textField('date0', 'From', 10, $date0) . $this->nbsp(2) 259 | . $this->textField('date1', 'To', 10, $date1); 260 | return $out; 261 | } 262 | function dateField($name, $ts) { 263 | return $this->textField($name, null, 10, $date); 264 | } 265 | function datetimeField($name, $ts) { 266 | $y = (int)substr($ts, 0, 4); 267 | $M = (int)substr($ts, 5, 2); 268 | $d = (int)substr($ts, 8, 2); 269 | $h = (int)substr($ts, 11, 2); 270 | $m = (int)substr($ts, 14, 2); 271 | $MList = [1=>'Jan', 2=>'Feb', 3=>'Mar', 4=>'Apr', 5=>'May', 6=>'Jun', 272 | 7=>'Jul', 8=>'Aug', 9=>'Sep', 10=>'Oct', 11=>'Nov', 12=>'Dec']; 273 | $dList = []; 274 | for($n=1; $n <= 31; $n++) 275 | $dList[$n] = $n; 276 | $hList = []; 277 | for($n=1; $n <= 12; $n++) 278 | $hList[$n] = $n; 279 | $mList = []; 280 | for($n=0; $n <= 59; $n++) 281 | $mList[$n] = sprintf('%02u', $n); 282 | $apList = ['AM', 'PM']; 283 | $ap = ($h >= 12) ? 1 : 0; 284 | if($h == 0) 285 | $h == 12; 286 | elseif($h > 12) 287 | $h -= 12; 288 | $s = ''; 289 | $out = '' . $s 290 | . $this->select($name . '[1]', null, $MList, $M, null, false) . $s 291 | . $this->select($name . '[2]', null, $dList, $d, null, false) . NBSP 292 | . $this->select($name . '[3]', null, $hList, $h, null, false) . ':' 293 | . $this->select($name . '[4]', null, $mList, $m, null, false) . NBSP 294 | . $this->select($name . '[5]', null, $apList, $ap, null, false); 295 | return $out; 296 | } 297 | function linkButton($title, $url, $class=null, $titleTag=null, $onClick=null, $target=null) { 298 | if($class) 299 | $class = " class=\"$class\""; 300 | if($titleTag) 301 | $titleTag = " title=\"$titleTag\""; 302 | if($onClick === null) { 303 | if($target) { 304 | $onClick = "let a=document.createElement('a'); a.href='$url'; a.target='$target'; a.click();"; 305 | } else { 306 | $onClick = "window.location.href='$url';"; 307 | } 308 | } 309 | return "\n"; 310 | } 311 | function cmdButton($title, $parms, $class=null, $url=null, $titleTag=null) { 312 | if(!$url) 313 | $url = getScriptName(); 314 | if($parms) 315 | $url .= '?' . http_build_query($parms, '', '&'); 316 | return $this->linkButton($title, $url, $class, $titleTag); 317 | } 318 | 319 | function fieldsetOpen($legend=null) { 320 | $out = "
\n"; 321 | if($legend) 322 | $out .= "" . htmlspecial($legend) . "\n"; 323 | return $out; 324 | } 325 | function fieldsetClose() { 326 | return "
\n"; 327 | } 328 | function formClose() { 329 | return ""; 330 | } 331 | // TABLES 332 | function tableOpen($hdrCols=null, $caption=null, $class=null, $escFunc=0, $id=null) { 333 | $class = $class ? " class=\"$class\"" : ''; 334 | $id = $id ? " id=\"$id\"" : ''; 335 | $out = ""; 336 | if($caption) 337 | $out .= "" . htmlspecial($caption) . "\n"; 338 | if($hdrCols) 339 | $out .= $this->tableHeader($hdrCols, $escFunc); 340 | return $out; 341 | } 342 | // escFunc=0 => htmlspecial() 343 | function tableHeader($cols, $escFunc=0) { 344 | $out = ""; 345 | foreach($cols as $col) 346 | $out .= "" . $this->escape($col, $escFunc) . "\n"; 347 | $out .= "\n"; 348 | return $out; 349 | } 350 | // escFunc=0 => htmlspecial() 351 | // cellAttr = Attributes indexed by column eg. [4=>'class="nodeNum" onClick="..."', ...] 352 | function tableRow($cols, $escFunc=0, $colWidth=null, $useTh=false, $cellAttr=null) { 353 | $cellTag = $useTh ? 'th' : 'td'; 354 | $out = ""; 355 | $i=0; 356 | foreach($cols as $n => $col) { 357 | unset($width); 358 | if(is_array($colWidth)) { 359 | if($colWidth[$i]) 360 | $width = $colWidth[$i]; 361 | $i++; 362 | } 363 | elseif($colWidth) 364 | $width = $colWidth; 365 | if(isset($width)) 366 | $width = " style=\"min-width:$width;\""; 367 | else 368 | $width = ''; 369 | $attr = ($cellAttr && array_key_exists($n, $cellAttr)) ? (' ' . $cellAttr[$n]) : ''; 370 | if($escFunc === null) 371 | $out .= "<$cellTag$width$attr>$col\n"; 372 | else 373 | $out .= "<$cellTag$width$attr>" . $this->escape($col, $escFunc) . "\n"; 374 | } 375 | $out .= "\n"; 376 | return $out; 377 | } 378 | function tableClose() { 379 | return "\n"; 380 | } 381 | // LISTS 382 | function ul($list) { 383 | $out = "
    "; 384 | foreach($list as $li) 385 | $out .= "
  • $li\n"; 386 | $out .= "
\n"; 387 | return $out; 388 | } 389 | 390 | // HEADERS 391 | function htmlOpen($title, $style=null) { 392 | $title = htmlspecial($title); 393 | $style = $style ? " style=\"$style\"" : ''; 394 | $out = "\n" 395 | . "\n" 396 | . "\n" 397 | . "\n" 398 | . "$title\n"; 399 | return $out; 400 | } 401 | function htmlClose() { 402 | return "\n"; 403 | } 404 | // Specify background color for html tag to prevent 'flash of white' before page rendered 405 | function head($title, $cssFiles=null, $bgcolor=null, $scripts=null, $extraLines=null) { 406 | $style = ($bgcolor !== null) ? "background-color:$bgcolor;" : null; 407 | $out = $this->htmlOpen($title, $style); 408 | if($cssFiles === null) 409 | $cssFiles = ['/css/main.css']; 410 | if(is_array($cssFiles)) { 411 | foreach($cssFiles as $file) 412 | $out .= "\n"; 413 | } 414 | $out .= '' . NL; 415 | $out .= '' . NL; 416 | if($scripts !== null) { 417 | $out .= $scripts; 418 | } else { 419 | // Fix firefox FOUC bug 420 | $out .= '' . NL; 421 | } 422 | if($extraLines !== null) 423 | $out .= $extraLines; 424 | $out .= "\n"; 425 | return $out; 426 | } 427 | // MISCELLANEOUS 428 | /* Escape contexts: 429 | - Within a tag e.g.

xxx

: htmlspecial() for non-html contents, none otherwise 430 | - Within a property e.g. alt="xxx": htmlattr(). Properties cannot contain html or double-quotes 431 | - Within a url: rawurlencode(). 432 | */ 433 | function escape($txt, $escFunc=null) { 434 | static $func = ['htmlspecial', 'htmlentities', 'urlencode', 'rawurlencode']; 435 | if($escFunc === null or $escFunc >= count($func)) 436 | return $txt; 437 | return $func[$escFunc]($txt); 438 | } 439 | function hr($width="100%") { 440 | return "
\n"; 441 | } 442 | 443 | function audio($url, $class=null, $preload=false, $center=false) { 444 | $class = $class ? " class=\"$class\"" : ''; 445 | $preload = $preload ? '' : ' preload="none"'; 446 | $out = "\n"; 447 | if($center) 448 | $out = $this->div($out, 'ac'); 449 | return $out; 450 | } 451 | 452 | function convertToHtml($txt) { 453 | $txt = htmlspecial($txt); 454 | $txt = preg_replace("/\n/", "
", $txt); 455 | return txt; 456 | } 457 | function cleanHtml($var) { 458 | if(is_array($var)) { 459 | foreach($var as $k => $v) { 460 | $newk = $this->cleanHtml($k); 461 | unset($var[$k]); 462 | $var[$newk] = $this->cleanHtml($v); 463 | } 464 | } elseif(is_object($var)) { 465 | $var = (object)$this->cleanHtml((array)$var); 466 | } elseif($var) { 467 | $var = str_replace(['
', ' '], ' ', $var); 468 | $var = str_replace('°', 'deg', $var); 469 | $var = str_replace('&', '&', $var); 470 | } 471 | return $var; 472 | } 473 | 474 | function setLabelClass($class) { 475 | $this->labelClass = $class; 476 | } 477 | private function getLabelHtml($text, $id) { 478 | if($this->labelClass) 479 | $lc = ' class="' . $this->labelClass . '"'; 480 | return "\n"; 481 | } 482 | 483 | } 484 | -------------------------------------------------------------------------------- /include/UserModel.php: -------------------------------------------------------------------------------- 1 | permission ?? PERMISSION_NONE; 29 | } 30 | 31 | class UserModel { 32 | const TABLENAME = 'user'; 33 | public $db; 34 | public $error; 35 | private $permissionList = [ 36 | PERMISSION_NONE => 'None (Login Disabled)', 37 | PERMISSION_READ_ONLY => 'Read Only', 38 | PERMISSION_READ_MODIFY => 'Read/Modify', 39 | PERMISSION_FULL => 'Full', 40 | PERMISSION_ADMIN => 'Admin', 41 | PERMISSION_SUPERUSER => 'Superuser']; 42 | 43 | function __construct($db) { 44 | $this->db = $db; 45 | } 46 | 47 | function getUsers($where=null, $orderBy=null, $minPermission=PERMISSION_NONE) { 48 | if($minPermission > PERMISSION_NONE) { 49 | if($where) 50 | $where = "permission >= $minPermission AND ($where)"; 51 | else 52 | $where = "permission >= $minPermission"; 53 | } 54 | $users = $this->db->getRecords(self::TABLENAME, $where, $orderBy); 55 | $this->checkDbError(__METHOD__); 56 | if(!_count($users)) 57 | return null; 58 | foreach($users as &$u) { 59 | // Validate last login date 60 | if($u->last_login < 1672964000) // Jan. 5, 23 61 | $u->last_login = 0; 62 | // Convert array parms 63 | $u->nodenums = csvToArray($u->nodenums); 64 | } 65 | unset($u); 66 | return $users; 67 | } 68 | 69 | function getUser($email, $minPermission=PERMISSION_NONE) { 70 | if(!$this->validateEmailAddress($email)) 71 | return null; 72 | $where = "`email`='$email'"; 73 | $users = $this->getUsers($where, null, $minPermission); 74 | if(is_array($users) && count($users)) 75 | return($users[0]); 76 | return null; 77 | } 78 | function getUserById($user_id, $minPermission=PERMISSION_NONE) { 79 | if(!validDbID($user_id)) 80 | return null; 81 | $where = "user.user_id=$user_id"; 82 | $users = $this->getUsers($where, null, $minPermission); 83 | if(is_array($users) && count($users)) 84 | return($users[0]); 85 | return null; 86 | } 87 | function add($u) { 88 | if(!$this->validateFields($u)) 89 | return null; 90 | if($u->pass) { 91 | if(!$this->validatePassword($u->pass)) 92 | return null; 93 | } else { 94 | // Generate passwd if none specified 95 | $u->pass = $this->generatePassword(); 96 | msg("Generated password for user \"$u->name\": \"$u->pass\". User will not be able to login without this."); 97 | } 98 | $u->hash = password_hash($u->pass, PASSWORD_BCRYPT); 99 | checkIntVals($u, ['timezone_id', 'last_login']); 100 | // Convert arrays to csv 101 | $u->nodenums = arrayToCsv($u->nodenums); 102 | $cols = ['name', 'hash', 'email', 'location', 'nodenums', 'permission', 'timezone_id']; 103 | $vals = [$u->name, $u->hash, $u->email, $u->location, $u->nodenums, $u->permission, $u->timezone_id]; 104 | $retval = $this->db->insertRow(self::TABLENAME, $cols, $vals); 105 | $this->checkDbError(__METHOD__); 106 | return $retval; 107 | } 108 | function update($u) { 109 | if(!validDbID($u->user_id)) 110 | return null; 111 | if(!$this->validateFields($u)) 112 | return null; 113 | checkIntVals($u, ['timezone_id', 'last_login']); 114 | // Convert arrays to csv 115 | $u->nodenums = arrayToCsv($u->nodenums); 116 | $cols = ['name', 'email', 'location', 'nodenums', 'permission', 'timezone_id']; 117 | $vals = [$u->name, $u->email, $u->location, $u->nodenums, $u->permission, $u->timezone_id]; 118 | if($u->last_login) { 119 | $cols[] = 'last_login'; 120 | $vals[] = $u->last_login; 121 | } 122 | if(!empty($u->pass)) { 123 | if($this->validatePassword($u->pass)) { 124 | $cols[] = 'hash'; 125 | $vals[] = password_hash($u->pass, PASSWORD_BCRYPT); 126 | } else { 127 | unset($this->error); 128 | } 129 | } 130 | $retval = $this->db->updateRow(self::TABLENAME, $cols, $vals, "user_id=$u->user_id"); 131 | $this->checkDbError(__METHOD__); 132 | return $retval; 133 | } 134 | function validateFields($u) { 135 | if(!$this->validateName($u->name)) 136 | return null; 137 | if($u->email && !$this->validateEmailAddress($u->email)) 138 | return null; 139 | if($u->location && !$this->validateLocation($u->location)) 140 | return null; 141 | if(!empty($u->nodenums) && !$this->validateNodenums($u->nodenums)) 142 | return null; 143 | return true; 144 | } 145 | function delete($u) { 146 | if(!validDbID($u->user_id)) 147 | return null; 148 | $retval = $this->db->deleteRows(self::TABLENAME, "user_id=$u->user_id"); 149 | if($retval) 150 | okMsg("Deleted user OK"); 151 | $this->checkDbError(__METHOD__); 152 | return $retval; 153 | } 154 | 155 | function getCount($where=null) { 156 | $retval = $this->db->getRecordCount(self::TABLENAME, $where); 157 | if(isset($this->db->error)) { 158 | $this->error = __METHOD__ .': '. $this->db->error; 159 | unset($this->db->error); 160 | } 161 | return $retval; 162 | } 163 | 164 | // Validate user login cookie. Not for processing login forms 165 | function validate() { 166 | $u = arrayToObj($_COOKIE, ['name', 'cpass', 'lexp']); 167 | if(!isset($u->name) || !isset($u->cpass) || !$u->name || !$u->cpass) 168 | return null; 169 | if(!$this->validateName($u->name)) 170 | return null; 171 | $users = $this->getUsers("name='$u->name'"); 172 | if(!$users || $this->error) 173 | return null; 174 | foreach($users as $user) { 175 | if($user->permission <= PERMISSION_NONE) { 176 | $this->error = 'Access Denied (account is currently disabled)'; 177 | } elseif($u->cpass === $this->cpass($user->hash)) { 178 | unset($this->error); 179 | break; 180 | } 181 | $this->error = 'Access Denied'; 182 | } 183 | if(isset($this->error)) 184 | return null; 185 | if(!isset($_GET['export']) && isset($_COOKIE['name']) && isset($_COOKIE['cpass'])) { 186 | // Periodically refresh cookies if expire in 15-35 days 187 | $age = ($u->lexp > 0) ? ($u->lexp - time()) / 86400.0 : 0; 188 | if($u->lexp <= 0 || ($age >= 15 && $age <= 35)) { 189 | $this->setLoginCookies($u->name, $u->cpass, true); 190 | } 191 | } 192 | $user->ip_addr = $_SERVER['REMOTE_ADDR']; 193 | if(!validIpAddr($user->ip_addr)) 194 | $user->ip_addr = 'Unknown'; 195 | // Update last login time / last IP Addr if changed 196 | $this->updateUserStats($user); 197 | return $user; 198 | } 199 | 200 | // Process login form submission 201 | function validateLogin($name, $pass, $remember) { 202 | usleep(rand(20000,500000)); // Prevent response time hack 203 | if(!$this->validateName($name)) { 204 | $this->error = 'Invalid Name / Call Sign'; 205 | return false; 206 | } 207 | if(!$this->validatePassword($pass)) { 208 | $this->error = 'Invalid Password'; 209 | return false; 210 | } 211 | $users = $this->getUsers("name='$name'"); 212 | if($this->error) 213 | return false; 214 | if(!$users) { 215 | $this->error = 'Invalid Login Name'; 216 | return false; 217 | } 218 | foreach($users as $u) { 219 | if($u->permission <= PERMISSION_NONE) { 220 | $this->error = 'Access Denied (account is currently disabled)'; 221 | } elseif(password_verify($pass, $u->hash)) { 222 | unset($this->error); 223 | $user = $u; 224 | break; 225 | } 226 | $this->error = 'Access Denied'; 227 | } 228 | if(isset($this->error)) 229 | return false; 230 | // Send cookies 231 | $this->setLoginCookies($user->name, $this->cpass($user->hash), $remember); 232 | return true; 233 | } 234 | 235 | function setLoginCookies($name, $cpass, $remember) { 236 | global $urlbase, $cookieSameSiteOpt, $cookieUseRootPath; 237 | // If 'remember me' set, set cookie for 45 days, otherwise 8 hours 238 | $exp = time() + ($remember ? 45*86400 : 8*3600); 239 | $path = $cookieUseRootPath ? '/' : "$urlbase/"; // eg. /allscan/ 240 | // PHP setcookie only supports opts array in >= v7.3.0 241 | if(PHP_VERSION_ID >= 70300) { 242 | $opts = ['expires'=>$exp, 'path'=>$path, 'samesite'=>$cookieSameSiteOpt]; 243 | setcookie("name", $name, $opts); 244 | setcookie("cpass", $cpass, $opts); 245 | setcookie("lexp", $exp, $opts); 246 | } else { 247 | // For < 7.3 PHP versions SameSite parm can be appended to the path parm 248 | $path .= '; SameSite=' . $cookieSameSiteOpt; 249 | setcookie("name", $name, $exp, $path); 250 | setcookie("cpass", $cpass, $exp, $path); 251 | setcookie("lexp", $exp, $exp, $path); 252 | } 253 | } 254 | 255 | function logout() { 256 | global $urlbase; 257 | setcookie("name", '', 1, "$urlbase/"); 258 | setcookie("cpass", '', 1, "$urlbase/"); 259 | setcookie("lexp", '', 1, "$urlbase/"); 260 | // Temp TBR: Also clear cookies at old path (pre-v0.52) 261 | if($urlbase) { 262 | setcookie("name", '', 1, "/"); 263 | setcookie("cpass", '', 1, "/"); 264 | setcookie("lexp", '', 1, "/"); 265 | } 266 | } 267 | 268 | private function updateUserStats($user) { 269 | if(!isset($user) || !validDbID($user->user_id)) 270 | return; 271 | $llChanged = (diffSecs($user->last_login) >= 3600); 272 | $ipChanged = ($user->ip_addr !== $user->last_ip_addr); 273 | if(!$llChanged && !$ipChanged) 274 | return true; 275 | 276 | $cols = ['last_login']; 277 | $vals = [time()]; 278 | if($ipChanged) { 279 | $cols[] = 'last_ip_addr'; 280 | $vals[] = $user->ip_addr; 281 | } 282 | $ret = $this->db->updateRow(self::TABLENAME, $cols, $vals, "user_id=$user->user_id"); 283 | $this->checkDbError(__METHOD__); 284 | return $ret; 285 | } 286 | 287 | function getPermissionList($maxPermission=PERMISSION_READ_ONLY, $minPermission=PERMISSION_NONE) { 288 | $val = []; 289 | for($i=$minPermission; $i <= $maxPermission; $i++) { 290 | if(isset($this->permissionList[$i])) 291 | $val[$i] = $this->permissionList[$i]; 292 | } 293 | return $val; 294 | } 295 | function getPermissionName($pval) { 296 | if($pval < PERMISSION_NONE) 297 | return null; 298 | if($pval > PERMISSION_SUPERUSER) 299 | $pval = PERMISSION_SUPERUSER; 300 | return $this->permissionList[$pval]; 301 | } 302 | // Below called from Settings page 303 | function changePassword($post) { 304 | global $user; 305 | if($post['pass'] !== $post['confirm']) { 306 | $this->error = 'Passwords do not match'; 307 | return false; 308 | } 309 | if(!$this->validatePassword($post['pass'])) 310 | return false; 311 | $user->pass = $post['pass']; 312 | $this->update($user); 313 | return !$this->checkDbError(__METHOD__); 314 | } 315 | 316 | function validateEmailAddress($email) { 317 | if(!validEmail($email)) { 318 | $this->error = 'Improperly formatted Email address'; 319 | return false; 320 | } 321 | return true; 322 | } 323 | function validateName($name) { 324 | if(preg_match('/^[a-zA-Z0-9-. ]{2,24}$/', $name) != 1) { 325 | $this->error = 'Improperly formatted Name. Must be 2-24 Alphanumeric characters'; 326 | return false; 327 | } 328 | return true; 329 | } 330 | function validateLocation($loc) { 331 | if(preg_match('|^[a-zA-Z0-9-.,/#& ]{2,64}$|', $loc) != 1) { 332 | $this->error = 'Improperly formatted Location. Must be 2-64 Alphanumeric characters'; 333 | return false; 334 | } 335 | return true; 336 | } 337 | function validateNodenums($nodenums) { 338 | foreach($nodenums as $n) { 339 | if($n < 1 || $n >= 5000000) { 340 | $this->error = "Improperly formatted node list. '$n' is not a valid node number"; 341 | return false; 342 | } 343 | } 344 | return true; 345 | } 346 | function validatePassword($p) { 347 | if(!$p || strlen($p) < 6 || preg_match('/^[a-zA-Z0-9~!@#$^&*,._-]{6,16}$/', $p) != 1) { 348 | $this->error = 'Invalid Password. Must be 6-16 printable ASCII characters'; 349 | return false; 350 | } 351 | return true; 352 | } 353 | private function generatePassword($length=10) { 354 | $letters = '1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ._-'; 355 | $randomString = ''; 356 | $len = strlen($letters); 357 | for($i=0; $i < $length; $i++) 358 | $randomString .= $letters[rand(0, $len-1)]; 359 | return $randomString; 360 | } 361 | private function cpass($p) { 362 | return hash('sha256', $p); 363 | } 364 | function checkDbError($method, $extraTxt='') { 365 | if(isset($this->db->error)) { 366 | if($extraTxt !== '') 367 | $extraTxt = "($extraTxt)"; 368 | $this->error = $method . $extraTxt . ': ' . $this->db->error; 369 | unset($this->db->error); 370 | return true; 371 | } 372 | return false; 373 | } 374 | 375 | } 376 | -------------------------------------------------------------------------------- /include/apiInit.php: -------------------------------------------------------------------------------- 1 | validate(); 25 | -------------------------------------------------------------------------------- /include/commonForms.php: -------------------------------------------------------------------------------- 1 | url)) 15 | $form->url = getScriptName(); 16 | if(!isset($form->method)) 17 | $form->method = 'post'; 18 | if(!isset($form->id)) 19 | $form->id = null; 20 | if(!isset($form->class)) 21 | $form->class = 'left'; 22 | if(!isset($form->target)) 23 | $form->target = null; 24 | $out = $html->formOpen($form->url, $form->method, $form->id, $form->class, $form->target); 25 | if(isset($form->fieldsetLegend)) 26 | $out .= $html->fieldsetOpen($form->fieldsetLegend); 27 | if(isset($form->hdr)) 28 | $out .= $form->hdr . NL; 29 | // Open table ($hdrCols=null, $caption=null, $class=null, $escFunc=0) 30 | if(!isset($form->tableClass)) 31 | $form->tableClass = 'noborder'; 32 | $table = $html->tableOpen(null, null, $form->tableClass); 33 | // Input fields 34 | $colwidths = ['5em', '', '']; 35 | // Submit button(s) 36 | $span = ''; 37 | if(!is_array($form->submit)) 38 | $form->submit = [$form->submit]; 39 | $style = (count($form->submit) > 3) ? 'small' : null; 40 | foreach($form->submit as $s) 41 | $span .= ' ' . $html->submitButton('Submit', $s, $style); 42 | $submit = $html->span($span, 'floatright'); 43 | // Form fields 44 | $count = $form->fields ? count($form->fields) : 0; 45 | if($count) { 46 | $fields = htmlFormGetFields($form->fields); 47 | $i = 0; 48 | foreach($fields as $title => $f) { 49 | if(strlen($title) <= 1) 50 | $title = ''; 51 | if($horizontal and ++$i == $count) 52 | $lastcol = $submit; 53 | else 54 | $lastcol = ' '; 55 | $table .= $html->tableRow([$title, $f, $lastcol], null, $colwidths); 56 | if($i) 57 | unset($colwidths); 58 | } 59 | } 60 | // formErrors 61 | if(isset($form->errMsg)) 62 | $table .= 'Error' 63 | . $form->errMsg . ''."\n"; 64 | if(!$horizontal) 65 | $table .= $html->tableRow(['', $submit, ''], null, false); 66 | if(isset($form->notes)) 67 | $table .= "$form->notes\n"; 68 | if(isset($form->floatright)) 69 | $class = 'floatright'; 70 | $table .= $html->tableClose(); 71 | $out .= $table; 72 | if(isset($form->note)) 73 | $out .= $html->span($form->note, 'gray'); 74 | if(isset($form->hiddenFields)) { 75 | foreach($form->hiddenFields as $k=>$v) 76 | $out .= $html->hiddenField($k, $v); 77 | } 78 | if(isset($form->fieldsetLegend)) 79 | $out .= $html->fieldsetClose(); 80 | $out .= $html->formClose(); 81 | if(isset($class)) 82 | $out = $html->div($out, $class); 83 | return $out; 84 | } 85 | 86 | function htmlSimpleForm($form, $labelOnTop=true) { 87 | global $html; 88 | if(!$form->method) 89 | $form->method = 'post'; 90 | if(!$form->url) 91 | $form->url = getScriptName(); 92 | if($form->floatright) 93 | $class = 'searchbar'; 94 | // Open form 95 | $out = $html->formOpen($form->url, $form->method, $form->id, $class); 96 | if($form->fieldsetLegend) 97 | $out .= $html->fieldsetOpen($form->fieldsetLegend); 98 | // formErrors 99 | if($form->errMsg) 100 | $out .= $html->span($form->errMsg, 'error') . BR; 101 | // Form fields 102 | $count = count($form->fields); 103 | if($count) { 104 | $fields = htmlFormGetFields($form->fields); 105 | foreach($fields as $title => $f) { 106 | if(strlen($title) <= 1) 107 | $title = ''; 108 | if($labelOnTop) { 109 | $out .= $html->div("$title
$f ", 'container'); 110 | } else { 111 | $out .= "$title $f \n"; 112 | } 113 | } 114 | } 115 | // Submit button(s) 116 | if(!is_array($form->submit)) 117 | $form->submit = [$form->submit]; 118 | foreach($form->submit as $s) 119 | $out .= $html->submitButton('Submit', $s) . ' '; 120 | if($form->fieldsetLegend) 121 | $out .= $html->fieldsetClose(); 122 | $out .= $html->formClose(); 123 | return $out; 124 | } 125 | 126 | function htmlFormGetFields($fields) { 127 | global $html; 128 | $outFields = []; 129 | foreach($fields as $title => $f) { 130 | $out = ''; 131 | foreach($f as $k=>$v) { 132 | $v0 = $v[0] ?? null; 133 | $v1 = $v[1] ?? null; 134 | $v2 = $v[2] ?? null; 135 | $v3 = $v[3] ?? null; 136 | switch($k[0]) { 137 | case 't': // text 138 | //textField($name, $label, $len=null, $val=null, $eol=false, $class=null, $readonly=null) 139 | if($k === 'textarea') 140 | $out .= $html->textArea($v0, null, $v1, $v2); 141 | elseif($k === 't128') 142 | $out .= $html->textField($v0, null, 128, $v1, 'wide'); 143 | elseif($k === 't64') 144 | $out .= $html->textField($v0, null, 64, $v1, 'wide'); 145 | elseif($k === 't3') 146 | $out .= $html->textField($v0, null, 3, $v1); 147 | else 148 | $out .= $html->textField($v0, null, null, $v1); 149 | break; 150 | case 'r': // readonly text 151 | $out .= $html->textField($v0, null, null, $v1, null, true); 152 | break; 153 | case 'p': // password 154 | $out .= $html->passwordField($v0, null); 155 | break; 156 | case 's': // select 157 | case 'm': // select multiple 158 | if(isset($v3)) // Don't escape vals 159 | $out .= $html->select($v0, null, $v1, $v2, null, $v3); 160 | else 161 | $out .= $html->select($v0, null, $v1, $v2); 162 | break; 163 | case 'c': // checkbox 164 | $out .= $html->checkbox($v0, null, $v1); 165 | break; 166 | case 'f': // file upload 167 | $out .= $html->fileUpload($v0, null, false, $v1); 168 | break; 169 | case 'F': // Multiple file upload 170 | $out .= $html->fileUpload($v0, null, true, $v1); 171 | break; 172 | case 'h': // hidden 173 | $out .= $html->hiddenField($v0, $v1); 174 | break; 175 | case 'd': // date 176 | $out = $html->dateField($v0, $v1); 177 | break; 178 | case 'D': // date range 179 | $out = $html->dateRange($v0, $v1); 180 | break; 181 | case 'T': // datetime 182 | $out = $html->datetimeField($v0, $v1); 183 | break; 184 | case 'L': // Line break 185 | $out = BR; 186 | break; 187 | case 'C': // Custom 188 | $out = $v; 189 | break; 190 | } 191 | } 192 | $outFields[$title] = $out; 193 | } 194 | return $outFields; 195 | } 196 | 197 | function confirmForm($label, $text, $parms=null, $url=null, $password=false, $submit=['Confirm', 'Cancel']) { 198 | global $html, $user, $userModel, $customerModel; 199 | $form = new stdClass(); 200 | $form->method = 'post'; 201 | if(!$url) 202 | $url = getScriptName(); 203 | // Open form 204 | $out = $html->formOpen($url, $form->method); 205 | if($label) 206 | $out .= $html->fieldsetOpen($label); 207 | // Text 208 | $out .= $html->p($text, null, false); 209 | // Password field 210 | if($password) 211 | $out .= $html->passwordField('password', 'Password'); 212 | // Parm Vals 213 | if(is_array($parms)) { 214 | foreach($parms as $key => $val) { 215 | if(is_array($val)) { 216 | $k = $key . '[]'; 217 | foreach($val as $v) 218 | $out .= $html->hiddenField($k, $v); 219 | } else { 220 | $out .= $html->hiddenField($key, $val); 221 | } 222 | } 223 | } 224 | // Submit button(s) 225 | $span = ''; 226 | if(!is_array($submit)) 227 | $submit = [$submit]; 228 | foreach($submit as $s) 229 | $span .= ' ' . $html->submitButton('Submit', $s); 230 | $out .= $html->span($span, 'floatright'); 231 | if($label) 232 | $out .= $html->fieldsetClose(); 233 | $out .= $html->formClose(); 234 | $out = $html->div($out); 235 | return $out; 236 | } 237 | -------------------------------------------------------------------------------- /include/dbUtils.php: -------------------------------------------------------------------------------- 1 | error); 25 | } 26 | return $db; 27 | } 28 | 29 | function initErrExit($msg) { 30 | global $asdbfile; 31 | pageInit(); 32 | echo implode(BR, $msg) . BR; 33 | msg("DB validation failed. $asdbfile may be corrupted. Try copying .bak over .db if exists or delete .db file."); 34 | asExit($db->error); 35 | } 36 | 37 | // checkTables() is called during page init prior to any headers or html output. 38 | // Verifies necessary DB Tables and columns exist, creates/updates tables as needed. 39 | // If no admin user account has been created yet user is redirected to the User module to create an account. 40 | // After logging in they can then change Cfg settings, User settings, and create additional user accounts. 41 | // If any errors occur msg will be written to $db->error and false will be returned 42 | function checkTables($db, &$msg) { 43 | global $createUserSql, $createCfgSql; 44 | // Verify users table exists, create if not 45 | $tables = $db->getTableList(); 46 | if($db->error) { 47 | $msg[] = "getTableList Error: $db->error"; 48 | initErrExit($msg); 49 | } 50 | $s = $tables ? implode(', ', $tables) : 'None found'; 51 | $msg[] = "DB Tables: $s"; 52 | if(!in_array('user', $tables)) { 53 | $msg[] = "Creating user DB Table"; 54 | $ret = $db->exec($createUserSql); 55 | if(!$ret) { 56 | $msg[] = "DB Error: $db->error"; 57 | initErrExit($msg); 58 | } 59 | } else { 60 | $cols = $db->getColList('user'); 61 | if($db->error) { 62 | $msg[] = "getColList(user) Error: $db->error"; 63 | initErrExit($msg); 64 | } 65 | //$s = $cols ? implode(', ', $cols) : 'None found'; 66 | //$msg[] = "User Table cols: $s"; 67 | } 68 | // Verify cfg table exists, create if not 69 | if(!in_array('cfg', $tables)) { 70 | $msg[] = "Creating cfg DB Table"; 71 | $ret = $db->exec($createCfgSql); 72 | if(!$ret) { 73 | $msg[] = "DB Error: $db->error"; 74 | initErrExit($msg); 75 | } 76 | } else { 77 | $cols = $db->getColList('cfg'); 78 | if($db->error) { 79 | $msg[] = "getColList(cfg) Error: $db->error"; 80 | initErrExit($msg); 81 | } 82 | //$s = $cols ? implode(', ', $cols) : 'None found'; 83 | //$msg[] = "Cfg Table cols: $s"; 84 | } 85 | // Return user count or false on error 86 | $cnt = $db->getRecordCount('user'); 87 | if($db->error) { 88 | $msg[] = "getRecordCount(user) Error: $db->error"; 89 | initErrExit($msg); 90 | } 91 | return $cnt; 92 | } 93 | 94 | $createUserSql = 'CREATE TABLE user ( 95 | user_id INTEGER PRIMARY KEY, 96 | name TEXT NOT NULL, 97 | hash TEXT NOT NULL, 98 | email TEXT, 99 | location TEXT, 100 | nodenums TEXT, 101 | permission INTEGER NOT NULL DEFAULT 1, 102 | timezone_id INTEGER NOT NULL DEFAULT 0, 103 | last_login INTEGER, 104 | last_ip_addr TEXT);'; 105 | 106 | $createCfgSql = 'CREATE TABLE cfg ( 107 | cfg_id INTEGER PRIMARY KEY, 108 | val TEXT NOT NULL, 109 | updated INTEGER NOT NULL);'; 110 | -------------------------------------------------------------------------------- /include/favsUtils.php: -------------------------------------------------------------------------------- 1 | formOpen(getRequestUri(), 'get'); 7 | $list = []; 8 | foreach($files as $f) 9 | $list[$f] = $f; 10 | $out .= $html->select('favsfile', 'Favorites File', $list, $activeFile, null, true, true); 11 | $out .= $html->formClose(); 12 | echo $out . NL; 13 | } 14 | 15 | function showFavsIniForm() { 16 | global $asdir; 17 | echo '
' . NL 18 | .'' . NL 19 | .'
' . BR; 20 | } 21 | 22 | function validateFavsFile($file, &$msg, $checkExists=true) { 23 | // Allow A-Z a-z 0-9 . _ - chars in name / suffix 24 | if(!preg_match('/^[\/\w\-. ]+$/', $file)) { 25 | $msg[] = error("Invalid filename"); 26 | return false; 27 | } 28 | if(!preg_match('/.ini$/', $file)) { 29 | $msg[] = error("Invalid filename suffix"); 30 | return false; 31 | } 32 | $name = basename($file, '.ini'); 33 | if(strpos($name, 'favorites') !== 0) { 34 | $msg[] = error("Invalid Favorites filename"); 35 | return false; 36 | } 37 | if($checkExists && !file_exists($file)) { 38 | $msg[] = error("Requested file not found"); 39 | return null; 40 | } 41 | return true; 42 | } 43 | 44 | // Determine favorites file location(s). Check gCfg[favsIniLoc] locations and any files 45 | // in the local dir or /etc/allscan/ dir named favorites*.ini 46 | function findFavsFiles(&$activeFile) { 47 | global $gCfg; 48 | $files = []; 49 | // Search for files in local dir and /etc/allscan/ dir named favorites*.ini 50 | $ldir = asDir(); 51 | $gdir = asDir(false); 52 | $lfiles = trim(shell_exec("ls {$ldir}favorites*.ini")); 53 | if($lfiles) { 54 | $files = explode(NL, $lfiles); 55 | } 56 | if($ldir !== $gdir) { 57 | $gfiles = trim(shell_exec("ls {$gdir}favorites*.ini")); 58 | if($gfiles) { 59 | $files = array_merge($files, explode(NL, $gfiles)); 60 | } 61 | } 62 | // Build list, determine active file (first file found) 63 | $activeFile = ''; 64 | foreach($gCfg[favsIniLoc] as $f) { 65 | if(!file_exists($f)) 66 | continue; 67 | // convert to absolute path 68 | $f = realpath($f); 69 | if(!$activeFile) 70 | $activeFile = $f; 71 | if(!in_array($f, $files)) 72 | $files[] = $f; 73 | } 74 | // Validate files 75 | $vfiles = []; 76 | foreach($files as $f) { 77 | $dbgmsg = []; 78 | if(validateFavsFile($f, $dbgmsg)) 79 | $vfiles[] = $f; 80 | } 81 | if(!$activeFile && count($vfiles)) 82 | $activeFile = $vfiles[0]; 83 | return $vfiles; 84 | } 85 | -------------------------------------------------------------------------------- /include/hwUtils.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/include/hwUtils.php -------------------------------------------------------------------------------- /include/logUtils.php: -------------------------------------------------------------------------------- 1 | p($msg, $class, $escapeTxt); 6 | } 7 | function h1($msg, $class=null, $escapeTxt=true) { 8 | global $html; 9 | echo $html->h(1, $msg, $class, $escapeTxt); 10 | } 11 | function h2($msg, $class=null, $escapeTxt=true) { 12 | global $html; 13 | echo $html->h(2, $msg, $class, $escapeTxt); 14 | } 15 | function h3($msg, $class=null, $escapeTxt=true) { 16 | global $html; 17 | echo $html->h(3, $msg, $class, $escapeTxt); 18 | } 19 | function h4($msg, $class=null, $escapeTxt=true) { 20 | global $html; 21 | echo $html->h(4, $msg, $class, $escapeTxt); 22 | } 23 | function pre($msg, $class=null, $escapeTxt=true, $style=null) { 24 | global $html; 25 | echo $html->pre($msg, $class, $escapeTxt, $style); 26 | } 27 | function br($n=1) { 28 | global $html; 29 | return $html->br($n); 30 | } 31 | function logToFile($msg, $file=null) { 32 | global $user; 33 | if($file === null) 34 | $file = 'errLog.txt'; 35 | if($msg === null || empty($msg)) 36 | $msg = '[null]'; 37 | elseif(is_object($msg) || is_array($msg)) 38 | $msg = varDump($msg, true); 39 | if(isset($user->name)) 40 | $name = $user->name; 41 | $tstamp = getCurTimestamp(); 42 | file_put_contents($file, "$tstamp: $name: $msg\n", FILE_APPEND); 43 | } 44 | function varDumpClean($var, $return=false) { 45 | $var = varDump($var, true); 46 | $res = explode("\n", $var); 47 | foreach($res as $i => $r) { 48 | if(preg_match('/[0-9a-zA-Z]/', $r) != 1) 49 | unset($res[$i]); 50 | } 51 | $res = implode("\n", $res); 52 | $res = str_replace("\t\t", "\t", $res); 53 | if($return) 54 | return $res; 55 | echo $res; 56 | } 57 | function displayArray($a, $class='results') { 58 | global $html; 59 | $keys = array_keys($a); 60 | $out = $html->tableOpen(null, null, $class, null); 61 | foreach($keys as $key) { 62 | if(!is_array($a[$key])) { 63 | $val = $a[$key]; 64 | } else { 65 | if(count($a[$key])) 66 | $val = displayArray($a[$key]); 67 | else 68 | $val = '[Empty]'; 69 | } 70 | $out .= $html->tableRow(["$key", $val], null); 71 | } 72 | $out .= $html->tableClose(); 73 | return $out; 74 | } 75 | -------------------------------------------------------------------------------- /include/timeUtils.php: -------------------------------------------------------------------------------- 1 | diff($now); 15 | if($diff->d || $diff->m || $diff->y || $diff->h > 21) 16 | $fmt = 'M j, Y g:i A'; 17 | else 18 | $fmt = 'g:i A'; 19 | } 20 | if($tz_id) 21 | $date->setTimezone(new DateTimeZone($timezoneDef[$tz_id])); 22 | return $date->format($fmt); 23 | } 24 | function convertTStoUTC($dt, $tz_id) { 25 | global $timezoneDef; 26 | if(!$tz_id) 27 | return $dt; 28 | $date = new DateTime($dt, new DateTimeZone($timezoneDef[$tz_id])); 29 | $date->setTimezone(new DateTimeZone('UTC')); 30 | return $date->format('Y-m-d H:i:s'); 31 | } 32 | // Return seconds difference from GMT of specified time zone 33 | function getTimeZoneOffset($tz) { 34 | $dtz = new DateTimeZone($tz); 35 | $dt = new DateTime('now', $dtz); 36 | return $dtz->getOffset($dt); 37 | } 38 | 39 | // TIME / DATE / TIMEZONE / CONVERSION FUNCTIONS 40 | function utcToServerTimestamp($ts) { 41 | return toDateTime(gmstrtotime($ts)); 42 | } 43 | function serverToUtcTimestamp($ts) { 44 | return toGmDateTime(strtotime($ts)); 45 | } 46 | // Converts UTC date time to unix timestamp 47 | function gmstrtotime($str) { 48 | return(strtotime($str . ' UTC')); 49 | } 50 | function togmt($ts) { 51 | $sec = gmdate('s', $ts); 52 | $min = gmdate('i', $ts); 53 | $hr = gmdate('H', $ts); 54 | $day = gmdate('d', $ts); 55 | $mon = gmdate('m', $ts); 56 | $yr = gmdate('Y', $ts); 57 | return mktime($hr, $min, $sec, $mon, $day, $yr); 58 | } 59 | function buildTimestamp($y, $mo, $d, $h, $m, $s) { // Result is in server timezone 60 | if($y < 100) 61 | $y += 2000; 62 | return date('Y-m-d H:i:s', mktime($h, $m, $s, $mo, $d, $y)); 63 | } 64 | function getTimestamp($time, $tzid=null, $fmt='Y-m-d H:i') { 65 | global $timezoneDef; 66 | if(is_numeric($time)) 67 | $time = '@' . $time; 68 | $date = new DateTime($time); 69 | if($tzid) 70 | $date->setTimezone(new DateTimeZone($timezoneDef[$tzid])); 71 | return $date->format($fmt); 72 | } 73 | function getCurTimestamp($tzid=null) { 74 | global $timezoneDef; 75 | $fmt = 'Y-m-d H:i:s'; 76 | if($tzid) { 77 | $date = new DateTime('now', new DateTimeZone($timezoneDef[$tzid])); 78 | return $date->format($fmt); 79 | } 80 | return date($fmt); 81 | } 82 | function getCurTime() { // Get cur time str (Server Timezone) 83 | return date('H:i:s'); 84 | } 85 | function getCurTimestampGmt() { // Get cur datetime str (GMT) 86 | return toGmDateTime(time()); 87 | } 88 | function toDateTime($ts) { // Convert Unix tstamp to Server Timezone datetime str 89 | return date('Y-m-d H:i:s', $ts); 90 | } 91 | function toGmDateTime($ts) { // Convert Unix tstamp to GMT datetime str 92 | return gmdate('Y-m-d H:i:s', $ts); 93 | } 94 | // Return # seconds into week starting Sunday 0000Z 95 | function gmGetSecsIntoWeek($ts=null) { 96 | // Unix tstamp is in UTC (mod by 86400 gives UTC hour, verified on a server set to MDT zone) 97 | if(!$ts) 98 | $ts = time(); 99 | // Unix time starts on Thursday. For 0=Sun...4=Thu..., Add 4 then mod 7 100 | return ($ts + 4 * SECS_PER_DAY) % SECS_PER_WEEK; 101 | } 102 | function diffSecs($str1, $str2=null) { // Returns: T2-T1 if T2 is set, otherwise time()-T1 103 | $ts1 = is_numeric($str1) ? $str1 : strtotime($str1); 104 | $ts2 = $str2 ? (is_numeric($str2) ? $str2 : strtotime($str2)) : time(); 105 | return $ts2 - $ts1; 106 | } 107 | function diffDays($str1, $str2=null) { // Returns: T2-T1 if T2 is set, otherwise time()-T1 in days 108 | return diffSecs($str1, $str2) / 86400.0; 109 | } 110 | function diffDaysFromGmt($str1) { 111 | return (time() - gmstrtotime($str1)) / 86400.0; 112 | } 113 | function timeCmp($ts1, $ts2) { // Returns T1 - T2 114 | return strtotime($ts1) - strtotime($ts2); 115 | } 116 | function timeDiffReadable($from, $to=null) { 117 | $to = ($to === null) ? time() : $to; 118 | $to = is_int($to) ? $to : strtotime($to); 119 | $from = is_int($from) ? $from : strtotime($from); 120 | $units = ['year' => 29030400, 'month' => 2419200, 'week' => 604800, 121 | 'day' => 86400, 'hr' => 3600, 'min' => 60, 'sec' => 1]; 122 | $diff = abs($from - $to); 123 | $suffix = ($from > $to) ? 'from now' : 'ago'; 124 | foreach($units as $unit => $mult) { 125 | if($diff >= $mult) { 126 | return intval($diff / $mult) . ' ' . $unit 127 | . ((intval($diff / $mult) == 1) ? '' : 's') . ' ' . $suffix; 128 | } 129 | } 130 | if($diff === 0) 131 | $diff = '0 sec ago'; 132 | return $diff; 133 | } 134 | -------------------------------------------------------------------------------- /include/timezones.php: -------------------------------------------------------------------------------- 1 | Connection Status 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
' . $title2 . '
  Node  Node InfoReceivedDirConnectedMode
Waiting...
21 |

22 | '; 23 | } 24 | 25 | function showNodeCtrlForm() { 26 | global $node, $remNode, $favsFile, $asdir, $gCfg; 27 | if(modifyOk()) 28 | echo '
29 | 30 | 31 | 32 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 |   44 | 46 |
47 | '; 48 | else 49 | echo '' . NL; 50 | } 51 | 52 | function showSetNodeInfoForm() { 53 | global $html, $user, $gCfgName, $gCfg; 54 | $form = new stdClass(); 55 | $form->fieldsetLegend = SET_NODE_INFO_CFGS; 56 | $form->submit = SET_NODE_INFO_CFGS; 57 | $form->fields = [ 58 | $gCfgName[call] => ['t' => ['call', $gCfg[call]]], 59 | $gCfgName[location] => ['t' => ['location', $gCfg[location]]], 60 | $gCfgName[title] => ['t' => ['title', $gCfg[title]]], 61 | ]; 62 | $form->id = 'nodeInfoForm'; 63 | echo htmlForm($form) . BR; 64 | } 65 | 66 | function showFooterLinks() { 67 | global $html; 68 | $out = []; 69 | $links = ['AllScan.info' => 'https://allscan.info/', 70 | 'Updates' => 'https://github.com/davidgsd/AllScan#allscan', 71 | 'AllStarLink.org' => 'https://www.allstarlink.org/', 72 | 'Keyed Nodes' => 'http://stats.allstarlink.org/stats/keyed', 73 | 'ASL Forum' => 'https://community.allstarlink.org/', 74 | 'AllScan FB' => 'https://www.facebook.com/groups/allscan', 75 | 'eHam.net' => 'https://www.eham.net/']; 76 | foreach($links as $title => $url) 77 | $out[] = $html->a($url, null, $title, null, '_blank') . NL; 78 | $sep = ' | '; 79 | echo $html->div(implode($sep, $out), 'm5'); 80 | } 81 | 82 | function getSortLink($urlparms, $sortCol, $sortAsc, $col, $colName='sortCol', $ordName='sortOrd') { 83 | global $html; 84 | $url = getScriptName(); 85 | $parms = $urlparms; 86 | // Start at page 1 when re-sorting 87 | unset($parms['page']); 88 | $parms[$colName] = $sortCol; 89 | $parms[$ordName] = $sortAsc ? 'a' : 'd'; 90 | return $html->a($url, $parms, $col, null, null, false); 91 | } 92 | 93 | function upDownArrow($up) { 94 | $s = $up ? '▲' : '▼'; 95 | return "$s"; 96 | } 97 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | validate(); 24 | if(!readOk()) 25 | redirect('user/'); 26 | 27 | $msg[] = "User: $user->name, IP: $user->ip_addr"; 28 | 29 | // Check disk free space 30 | checkDiskSpace($msg); 31 | 32 | // Load node and host definitions 33 | $hosts = []; 34 | $onLoad = ''; 35 | $nodes = getNodeCfg($msg, $hosts); 36 | if(!empty($nodes) && !empty($hosts)) { 37 | $node = $nodes[0]; 38 | $host = $hosts[0]; 39 | 40 | // Load ASL DB 41 | $astdb = readAstDb($msg); 42 | 43 | if($astdb !== false) 44 | $onLoad = " onLoad=\"asInit('server.php?nodes=$node')\""; 45 | 46 | // Handle form submits 47 | $parms = getRequestParms(); 48 | if(isset($_POST['Submit']) && $astdb !== false) { 49 | if(isset($parms['favsfile']) && $parms['favsfile']) 50 | $favsFile = $parms['favsfile']; 51 | processForm($parms, $msg); 52 | // Reset parms after processing post submits 53 | unset($_POST, $parms); 54 | if(isset($favsFile)) 55 | $parms = ['favsfile' => $favsFile]; 56 | } 57 | 58 | // Determine favorites file location(s) 59 | $favsFile = ''; 60 | $favsFiles = findFavsFiles($favsFile); 61 | if(!empty($favsFiles)) { 62 | if(isset($parms['favsfile']) && in_array($parms['favsfile'], $favsFiles)) 63 | $favsFile = $parms['favsfile']; 64 | } else { 65 | unset($favsFile); 66 | } 67 | } 68 | 69 | pageInit($onLoad, true, checkUpdate()); 70 | 71 | if(!isset($node) || $astdb === false) 72 | asExit(implode(BR, $msg)); 73 | 74 | if(!$gCfg[call] && adminUser()) { 75 | p('Node Call Sign, Location and/or Title have not yet been set. Enter these below:'); 76 | showSetNodeInfoForm(); 77 | } 78 | 79 | if(!isset($parms)) 80 | $parms = []; 81 | 82 | $remNode = (isset($parms['node']) && validDbID($parms['node']) && strlen($parms['node']) < 9) ? $parms['node'] : ''; 83 | 84 | showConnStatusTable(); 85 | showNodeCtrlForm(); 86 | 87 | h2('Favorites'); 88 | // Read in favorites.ini 89 | $favs = []; 90 | $favcmds = []; 91 | if(!isset($favsFile) || !$favsFile) { 92 | msg('favorites.ini not found. Click below to create ' . $gCfg[favsIniLoc][0]); 93 | showFavsIniForm(); 94 | } else { 95 | $favsIni = parse_ini_file($favsFile, true); 96 | if($favsIni === false) { 97 | p("Error parsing $favsFile. Check file format/permissions or create file with www-data writeable permissions."); 98 | } else { 99 | // Combine [general] stanza with this node's stanza 100 | $favsCfg = $favsIni['general']; 101 | if(isset($favsIni[$node])) { 102 | foreach($favsIni[$node] as $type => $arr) { 103 | if($type == 'label') { 104 | foreach($arr as $label) { 105 | $favsCfg['label'][] = $label; 106 | } 107 | } elseif($type == 'cmd') { 108 | foreach($arr as $cmd) { 109 | $favsCfg['cmd'][] = $cmd; 110 | } 111 | } 112 | } 113 | } 114 | $favsCfg['label'] = array_map('trim', $favsCfg['label']); 115 | $favsCfg['cmd'] = array_map('trim', $favsCfg['cmd']); 116 | foreach($favsCfg['cmd'] as $i => $c) { 117 | if(!$c) { 118 | unset($favsCfg['cmd'][$i], $favsCfg['label'][$i]); 119 | } else { 120 | if(preg_match('/[0-9]{4,8}/', $c, $m) == 1) 121 | $favs[$i] = (object)['node'=>$m[0], 'label'=>$favsCfg['label'][$i], 'cmd'=>$c]; 122 | else 123 | $favcmds[$i] = (object)['label'=>$favsCfg['label'][$i], 'cmd'=>$c]; 124 | } 125 | } 126 | // if(count($favcmds)) 127 | // varDump($favcmds); 128 | $msg[] = _count($favs) . " favorites read from $favsFile"; 129 | } 130 | } 131 | // Combine favs node, label data with astdb data into favList 132 | $favList = []; 133 | foreach($favs as $n => $f) { 134 | if(array_key_exists($f->node, $astdb)) { 135 | list($x, $call, $desc, $loc) = $astdb[$f->node]; 136 | } else { 137 | if($f->node < 3000000) { 138 | list($x, $call, $desc, $loc) = [$n, '-', '[Not in ASL DB]', '[Check Node Number]']; 139 | } else { 140 | $info = getELInfo($f->node); 141 | if(empty($info)) 142 | list($x, $call, $desc, $loc) = [$n, '-', '[EchoLink Node]', '-']; 143 | else { 144 | if(preg_match('/(.*) (\[.*\])/', $info, $m) != 1) 145 | $m = [1=>'-', 2=>"[EchoLink $f->node]"]; 146 | list($x, $call, $desc, $loc) = [$n, $m[1], $m[2], '-']; 147 | } 148 | } 149 | } 150 | $name = str_replace([$f->node, $call, $desc, $loc, ' ,'], ' ', $f->label); 151 | //$msg[] = "$x, $call, $desc, $loc, $f->label"; 152 | //$msg[] = $name; 153 | foreach(['call', 'name', 'desc', 'loc'] as $var) 154 | $$var = trim(str_replace(' ', ' ', $$var), " .,;\n\r\t\v\x00"); 155 | if(!$name) 156 | $name = $call; 157 | elseif(strpos($name, $call) === false && $call !== '-') 158 | $name = $call . ' ' . $name; 159 | //$msg[] = $name; 160 | $favList[] = [$n, $f->node, $name, $desc, $loc, NBSP, NBSP]; 161 | } 162 | // Sort favList by specified column if fs parm is set 163 | $colKey = ['num', 'node', 'name', 'desc', 'loc']; 164 | $sortCol = isset($_GET['fs']) && in_array($_GET['fs'], $colKey) ? $_GET['fs'] : 'num'; 165 | if($sortCol && !empty($favList) && count($favList) > 1) { 166 | $col = array_search($sortCol, $colKey); 167 | $sortAsc = !(isset($_GET['fso']) && $_GET['fso'] === 'd'); 168 | $favList = sortArray($favList, $col, !$sortAsc); 169 | } 170 | // Output Favorites table 171 | if(empty($favList)) { 172 | p('No Favorites have yet been added'); 173 | } else { 174 | $hdrCols = ['#', 'Node', 'Name', 'Desc', 'Location', 'Rx%', 'LCnt']; 175 | if(count($favList) > 1) { 176 | foreach($hdrCols as $key => &$col) { 177 | if($key > 4) 178 | break; 179 | $ck = $colKey[$key]; 180 | if($sortCol === $ck) { 181 | // Link to sort in opposite order 182 | $col = getSortLink($parms, $ck, !$sortAsc, $col, 'fs', 'fso'); 183 | // Show an arrow indicating current sort col and direction 184 | $col .= upDownArrow($sortAsc); 185 | } else { 186 | // Link to sort in ASC order (or DESC order for time cols) 187 | $col = getSortLink($parms, $ck, true, $col, 'fs', 'fso'); 188 | } 189 | } 190 | } 191 | $out = $html->tableOpen($hdrCols, null, 'favs', null, 'favs'); 192 | foreach($favList as $f) { 193 | $nodeNumAttr = ['1' => 'class="nodeNum" onClick="setNodeBox('.$f[1].')" ' 194 | . 'onDblClick="connectNode(\'connect\')"']; 195 | // Link name to ASL stats page for node 196 | if($f[1] >= 2000 && $f[1] < 3000000) 197 | $f[2] = $html->a("http://stats.allstarlink.org/stats/" . $f[1], null, $f[2], null, 'stats'); 198 | if($f[3] == '') 199 | $f[3] = '-'; 200 | if($f[4] == '') 201 | $f[4] = '-'; 202 | $out .= $html->tableRow($f, null, null, false, $nodeNumAttr); 203 | } 204 | $out .= $html->tableClose(); 205 | echo $out; 206 | } 207 | 208 | // Show Favorites File select control 209 | showFavsSelect($favsFiles, $favsFile); 210 | 211 | // Status Messages div 212 | echo "

"; 213 | $msg = implode(BR, $msg); 214 | echo "
$msg
" . BR; 215 | 216 | $sep = ENSP . '|' . ENSP; 217 | // Show CPU Temp if available 218 | if(($ct = cpuTemp())) 219 | echo '' . $ct . '' . $sep . NL; 220 | 221 | // Show function buttons and Links 222 | echo $html->linkButton('Node Stats', "http://stats.allstarlink.org/stats/$node", 'small', null, null, 'stats'); 223 | 224 | if(modifyOk()) 225 | echo $html->linkButton('Restart Asterisk', null, 'small', null, 'astrestart();'); 226 | 227 | showFooterLinks(); 228 | 229 | asExit(); 230 | 231 | // --------------------------------------------------- 232 | function processForm($parms, &$msg) { 233 | global $astdb, $favsFile, $gCfg, $cfgModel; 234 | $node = $parms['node']; 235 | switch($parms['Submit']) { 236 | case "Add Favorite": 237 | $msg[] = "Add Node $node to Favorites requested"; 238 | if(!validDbID($node)) { 239 | $msg[] = "Invalid node#."; 240 | break; 241 | } 242 | if(!array_key_exists($node, $astdb) && ($nodeNum >= 2000 && $nodeNum < 3000000)) { 243 | $msg[] = "Node $node not found in ASL DB. Check Node Number and that your astdb file is up-to-date."; 244 | break; 245 | } 246 | // Parse file lines and add new favorite after last label,cmd lines. 247 | // Note: Does not look at [general] or [node#] sections (TBI) 248 | if(isset($favsFile)) { 249 | if(($favs = readFileLines($favsFile, $msg, true)) === false) 250 | break; 251 | $n = count($favs); 252 | $msg[] = "$n lines read from $favsFile"; 253 | } else { 254 | $favsFile = $gCfg[favsIniLoc][0]; 255 | $favs = ['[general]', '']; 256 | $n = count($favs); 257 | } 258 | $insertLn = 0; 259 | for($i=0; $i < $n; $i++) { 260 | if(strpos($favs[$i], 'cmd[]') === 0) { 261 | if(strpos($favs[$i], " $node\"")) { 262 | $msg[] = "Node $node already exists in favorites."; 263 | break(2); 264 | } 265 | $insertLn = $i + 2; 266 | } 267 | } 268 | if(!$insertLn) 269 | $insertLn = $n; 270 | // Add blank line after last fav entry if not present 271 | if($favs[$insertLn] !== '') { 272 | array_splice($favs, $insertLn, 0, ['']); 273 | $n++; 274 | } 275 | list($x, $call, $desc, $loc) = $astdb[$node]; 276 | $label = "label[] = \"$call $desc, $loc $node\""; 277 | $cmd = "cmd[] = \"rpt cmd %node% ilink 3 $node\""; 278 | array_splice($favs, $insertLn, 0, [$label, $cmd, '']); 279 | $n = count($favs); 280 | if(!writeFileLines($favsFile, $favs, $msg)) 281 | break; 282 | $msg[] = "Successfully wrote $n lines to $favsFile"; 283 | break; 284 | case "Delete Favorite": 285 | $msg[] = "Delete Node $node from Favorites requested"; 286 | if(!isset($favsFile)) { 287 | $msg[] = "Favorites file does not exist."; 288 | break; 289 | } 290 | if(!validDbID($node)) { 291 | $msg[] = "Invalid node#."; 292 | break; 293 | } 294 | // Parse file lines and delete favorite's label,cmd lines. 295 | // Note: Does not look for [general] or [node#] sections 296 | if(($favs = readFileLines($favsFile, $msg, true)) === false) 297 | break; 298 | $n = count($favs); 299 | $msg[] = "$n lines read from $favsFile"; 300 | for($i=0; $i < $n; $i++) { 301 | if(strpos($favs[$i], 'cmd[]') == 0 && strpos($favs[$i], " $node\"")) 302 | $delLn = $i + 1; 303 | } 304 | if(!isset($delLn)) { 305 | $msg[] = "Node $node not found in $favsFile"; 306 | break; 307 | } 308 | $nLines = 2; 309 | $startLn = $delLn - $nLines; 310 | if($startLn <= 0) { 311 | $msg[] = error("Invalid $favsFile format"); 312 | break; 313 | } 314 | // Also delete blank line after entry if present 315 | if($favs[$delLn] === '' && $delLn < $n - 1) 316 | $nLines++; 317 | for($i=0; $i < $nLines; $i++) 318 | unset($favs[$startLn + $i]); 319 | $n = count($favs); 320 | if(!writeFileLines($favsFile, $favs, $msg)) 321 | break; 322 | $msg[] = "Successfully wrote $n lines to $favsFile"; 323 | break; 324 | case SET_NODE_INFO_CFGS: 325 | if(!isset($parms['call']) || !isset($parms['location']) || !isset($parms['title'])) 326 | break; 327 | $gCfg[call] = $parms['call']; 328 | $gCfg[location] = $parms['location']; 329 | $gCfg[title] = $parms['title']; 330 | $cfgModel->saveCfgs(); 331 | if($cfgModel->error) 332 | $msg[] = error('Error saving Node Info Cfgs: ' . $cfgModel->error); 333 | else 334 | $msg[] = 'Saved Node Info Cfgs OK'; 335 | break; 336 | case CREATE_FAVORITESINI_FILE: 337 | $from = 'docs/favorites.ini.sample'; 338 | $to = $gCfg[favsIniLoc][0]; 339 | if(copy($from, $to)) { 340 | $msg[] = "Copied $from to $to OK"; 341 | chmod($to, 0664); 342 | $favsFile = $to; 343 | } else { 344 | $msg[] = error("Copy $from to $to Error. Check directory permissions."); 345 | } 346 | break; 347 | } 348 | } 349 | 350 | function checkUpdate() { 351 | global $msg; 352 | if(!adminUser()) 353 | return false; 354 | $fname = "include/common.php"; 355 | $vpat = '/^\$AllScanVersion = "v([0-9\.]{3,4})"/'; 356 | if(!file_exists($fname)) 357 | return true; 358 | $file = file($fname); 359 | if(empty($file)) 360 | return true; 361 | foreach($file as $line) { 362 | if(preg_match($vpat, $line, $m) == 1) { 363 | $vl = $m[1]; 364 | break; 365 | } 366 | } 367 | if(empty($vl)) 368 | return true; 369 | 370 | $url = "https://raw.githubusercontent.com/davidgsd/AllScan/main/$fname"; 371 | $file = file($url); 372 | if(empty($file)) { 373 | $msg[] = "Unable to retrieve $url"; 374 | return false; 375 | } 376 | foreach($file as $line) { 377 | if(preg_match($vpat, $line, $m) == 1) { 378 | $vr = $m[1]; 379 | break; 380 | } 381 | } 382 | if(empty($vr)) { 383 | $msg[] = "Error parsing $url"; 384 | return false; 385 | } 386 | 387 | $vl = (float)trim($vl); 388 | $vr = (float)trim($vr); 389 | if($vl != $vr) { 390 | $msg[] = "AllScan v$vr is now available, click the Update link for more info."; 391 | return true; 392 | } 393 | return false; 394 | } 395 | 396 | function getELInfo($n) { 397 | global $node, $host, $ami; 398 | static $servers, $fp, $cfg; 399 | if(!$node || !$host) { 400 | return; 401 | } 402 | if(empty($ami)) { 403 | $ami = new AMI(); 404 | $servers = []; 405 | $fp = []; 406 | $cfg = readNodeCfg(); 407 | } 408 | // Login to AMI 409 | if(!array_key_exists($host, $servers)) { 410 | // msg("Connecting to Asterisk Manager $node $host..."); 411 | $fp[$host] = $ami->connect($host); 412 | if($fp[$host] === false) { 413 | //msg('Connect Failed. Check allmon.ini settings.'); 414 | return; 415 | } 416 | $amiuser = $cfg[$node]['user']; 417 | $pass = $cfg[$node]['passwd']; 418 | if($ami->login($fp[$host], $amiuser, $pass) !== false) { 419 | $servers[$host] = 'y'; 420 | //msg('Login OK'); 421 | } else { 422 | //msg("Login Failed. Check allmon.ini settings."); 423 | return; 424 | } 425 | } 426 | return getAstInfo($fp[$host], $n); 427 | } 428 | 429 | function sortArray($list, $col, $desc) { 430 | $colVals = []; 431 | foreach($list as $val) 432 | $colVals[] = $val[$col]; 433 | // Sort column, retain keys 434 | $opt = $col ? (SORT_STRING | SORT_FLAG_CASE) : SORT_REGULAR; 435 | if($desc) 436 | arsort($colVals, $opt); 437 | else 438 | asort($colVals, $opt); 439 | // Reorder input array 440 | $out = []; 441 | foreach(array_keys($colVals) as $k) 442 | $out[] = $list[$k]; 443 | return $out; 444 | } 445 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgsd/AllScan/3cc2a49cc08766d87e304a5bbd677e12a95a38f8/screenshot.png -------------------------------------------------------------------------------- /stats/stats.php: -------------------------------------------------------------------------------- 1 | 'Insufficient user permission']); 6 | exit(); 7 | } 8 | 9 | // Filter and validate user input 10 | if((!isset($_POST['node']) || !$_POST['node']) && (!isset($_POST['nodes']) || !$_POST['nodes'])) { 11 | sendData(['status' => 'No node(s) specified']); 12 | exit(); 13 | } 14 | if(isset($_POST['node']) && $_POST['node']) { 15 | $node = trim(strip_tags($_POST['node'])); 16 | $nodes = [$node]; 17 | } else { 18 | $nodes = trim(strip_tags($_POST['nodes'])); 19 | $nodes = explode(',', $nodes); 20 | } 21 | foreach($nodes as $n) { 22 | if(!validDbID($n)) { 23 | sendData(['status' => "Invalid node '$n' specified in nodes list"]); 24 | } 25 | } 26 | $cnt = count($nodes); 27 | $stats = []; 28 | $time = time(); 29 | 30 | // For now only one node will be requested at a time 31 | foreach($nodes as $n) { 32 | if($n < 2000 || $n >= 2000000) { 33 | sendData(['status' => "Node $n not a public node"], 'stats'); 34 | continue; 35 | } 36 | $error = ''; 37 | $retcode = 0; 38 | $url = "http://stats.allstarlink.org/api/stats/$n"; 39 | $data = doWebRequest($url, null, $error, $retcode); 40 | if(!$data) { 41 | sendData(['status' => "ASL stats $error", 'node' => $n, 'retcode' => $retcode], 'stats'); 42 | break; 43 | } 44 | $resp = json_decode($data); 45 | $s = parseStats($resp, $time); 46 | if(!isset($s->node)) { 47 | $s->node = $n; 48 | $s->active = $s->keyed = $s->uptime = $s->txtime = $s->keyups = $s->kerchunks = $s->txperday = 0; 49 | $s->busyPct = $s->linkCnt = $s->avgTx = '-'; 50 | } 51 | $time = $s->timeAgo ?? '-'; 52 | $uph = round($s->uptime/3600) . 'h'; 53 | $txh = round($s->txtime/3600) . 'h'; 54 | // Data structure: event=stats; status=LogMsg; stats=statsStruct 55 | $msg = "$n Act=$s->active $time $uph Tx=$s->keyed $txh $s->keyups/$s->kerchunks $s->txperday/d {$s->avgTx}s " 56 | . "Rx%=$s->busyPct LCnt=$s->linkCnt WT=$s->wt"; 57 | sendData(['status' => $msg, 'stats' => $s], 'stats'); 58 | } 59 | 60 | exit(); 61 | 62 | function parseStats($resp, $time) { 63 | $s = new stdClass(); 64 | if(isset($resp->stats)) { 65 | $stats = $resp->stats; 66 | $s->node = $stats->node; 67 | $data = $stats->data; 68 | $s->uptime = $data->apprptuptime; 69 | $s->keyups = $data->totalkeyups; 70 | $s->txtime = $data->totaltxtime; 71 | $s->kerchunks = $data->totalkerchunks; 72 | $s->avgTx = ($s->txtime && $s->keyups) ? round($s->txtime/$s->keyups) : '-'; 73 | $s->txperday = ($s->uptime > 60 && $s->keyups) ? round($s->keyups/($s->uptime/86400)) : '-'; 74 | $s->busyPct = ($s->uptime > 60 && $s->txtime > 10) ? round(100 * $s->txtime / $s->uptime) : '-'; 75 | $s->linkCnt = _count($data->links); 76 | $s->keyed = $data->keyed ? '1' : '0'; 77 | $s->time = $data->time; 78 | $s->timeAgo = max($time - $data->time, 0); 79 | unset($data->linkedNodes); 80 | $s->updated = strtotime($stats->updated_at); 81 | unset($stats->user_node); 82 | } 83 | if(isset($resp->node)) { 84 | $node = $resp->node; 85 | $s->regtime = $node->regseconds; 86 | $s->status = $node->Status; 87 | $s->active = ($s->status === 'Active') ? '1' : '0'; 88 | $s->wt = $node->access_webtransceiver; 89 | } 90 | return $s; 91 | } 92 | 93 | function doWebRequest($url, $parms=null, &$error, &$retcode) { 94 | //$httpCodes = [400 => 'Timeout', 403 => 'Forbidden', 404 => 'Error', 429 => 'Too Many Requests']; 95 | if(function_exists('curl_init')) { 96 | $ch = curl_init(); 97 | $timeout = 10; 98 | $def = [ 99 | CURLOPT_URL => $url, 100 | CURLOPT_RETURNTRANSFER => true, 101 | CURLOPT_CONNECTTIMEOUT => $timeout, 102 | CURLOPT_TIMEOUT => $timeout, 103 | ]; 104 | curl_setopt_array($ch, $def); 105 | if($parms !== null) { 106 | curl_setopt($ch, CURLOPT_POST, true); 107 | $p = []; 108 | foreach($parms as $k=>$v) 109 | $p[] = "$k=$v"; 110 | curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $p)); 111 | } 112 | $result = curl_exec($ch); 113 | if(!$result) { 114 | $errno = curl_errno($ch); 115 | if($errno == 28) { 116 | $error = "Network response timeout"; 117 | } else { 118 | $err = curl_error($ch); 119 | if($err) 120 | $error = "HTTP Error: $err"; 121 | else 122 | $error = "Unknown HTTP error"; 123 | } 124 | curl_close($ch); 125 | return null; 126 | } 127 | $retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 128 | curl_close($ch); 129 | } else { 130 | ini_set('default_socket_timeout', 10); 131 | $result = @file_get_contents($url); 132 | preg_match('/([0-9])\d+/', $http_response_header[0], $matches); 133 | $retcode = intval($matches[0]); 134 | } 135 | if($retcode != 200) { 136 | //$txt = array_key_exists($rc, $httpCodes) ? "$rc ($httpCodes[$rc])" : $rc; 137 | $error = "HTTP return code $retcode"; 138 | return null; 139 | } 140 | return $result; 141 | } 142 | 143 | function sendData($data, $event='errMsg') { 144 | $resp = ['event' => $event, 'data' => $data]; 145 | echo json_encode($resp); 146 | ob_flush(); 147 | flush(); 148 | } 149 | -------------------------------------------------------------------------------- /user/UserView.php: -------------------------------------------------------------------------------- 1 | tableOpen($hdrCols, null, 'favs', null); 14 | $nullVal = '-'; 15 | foreach($users as $u) { 16 | if(isset($u->last_login) && $u->last_login) { 17 | $lastLogin = getTimestamp($u->last_login, $user->timezone_id, 'Y-m-d'); 18 | } else { 19 | $lastLogin = $nullVal; 20 | } 21 | $loc = $u->location ? $u->location : $nullVal; 22 | // TBI: link node#'s to ASL stats 23 | $nn = empty($u->nodenums) ? $nullVal : implode(' ', $u->nodenums); 24 | $perm = $userModel->getPermissionName($u->permission); 25 | $row = [$u->name, $u->email, $loc, $nn, $lastLogin, $perm, $timezoneDef[$u->timezone_id], $u->user_id]; 26 | $out .= $html->tableRow($row, null); 27 | } 28 | $out .= $html->tableClose(); 29 | //$out = $html->div($out, 'center'); 30 | echo $out . BR; 31 | } 32 | 33 | function showForms($newUser) { 34 | global $html, $user, $userModel, $timezoneDef; 35 | $form = new stdClass(); 36 | if(isset($newUser->errMsg)) 37 | $form->errMsg = $newUser->errMsg; 38 | // Get user from DB if edit request 39 | if($newUser->edit) { 40 | $newUser = $userModel->getUserById($newUser->user_id); 41 | if(isset($userModel->error)) 42 | errMsg($userModel->error); 43 | $newUser->edit = true; // Restore attr 44 | } 45 | if(empty($user)) { 46 | h1("Welcome to AllScan - Initial Configuration"); 47 | msg("No users have yet been created."); 48 | p("If you are an authorized admin of this server, create your user account now." . BR 49 | ."Be sure to carefully read all Notes on this page before continuing.", null, false); 50 | $newUser->permission = PERMISSION_SUPERUSER; 51 | $permList = $userModel->getPermissionList(PERMISSION_SUPERUSER, PERMISSION_SUPERUSER); 52 | } else { 53 | // Users may not assign permissions > their own 54 | $maxPerm = userPermission(); 55 | if($maxPerm > PERMISSION_SUPERUSER) 56 | $maxPerm = PERMISSION_SUPERUSER; 57 | $minPerm = PERMISSION_NONE; 58 | $permList = $userModel->getPermissionList($maxPerm, $minPerm); 59 | if(!isset($newUser->permission) || $newUser->permission < 0 || $newUser->permission > $maxPerm) 60 | $newUser->permission = PERMISSION_READ_ONLY; 61 | } 62 | // Fill in form fields array 63 | $form->fields = [ 64 | 'Name' => ['text' => ['name', $newUser->name]], 65 | 'Email' => ['text' => ['email', $newUser->email]], 66 | 'Password' => ['text' => ['pass']], 67 | 'Location' => ['text' => ['location', $newUser->location]], 68 | 'Node #s' => ['text' => ['nodenums', implode(' ', $newUser->nodenums)]], 69 | 'Permission' => ['select' => ['permission', $permList, $newUser->permission]], 70 | 'Time Zone' => ['select' => ['timezone_id', $timezoneDef, $newUser->timezone_id]]]; 71 | // If an edit request make appropriate adjustments to the form 72 | if($newUser->edit) { 73 | $form->submit = $form->fieldsetLegend = EDIT_USER; 74 | $form->id = 'editUserForm'; 75 | $form->hiddenFields['user_id'] = $newUser->user_id; 76 | echo htmlForm($form) . BR; 77 | } else { 78 | // Display Add User Form 79 | if(userPermission() >= PERMISSION_FULL || empty($user)) { 80 | $form->submit = $form->fieldsetLegend = ADD_USER; 81 | $form->id = 'addUserForm'; 82 | echo htmlForm($form) . BR; 83 | } 84 | // Display Edit request form, showing only users who can be edited by this user 85 | if(!empty($user)) { 86 | $where = superUser($user) ? "user_id != $user->user_id" : "permission < $user->permission"; 87 | $users = $userModel->getUsers($where, null, PERMISSION_NONE); 88 | if(isset($userModel->error)) 89 | errMsg($userModel->error); 90 | // If there are users that can be edited display edit/delete form 91 | if(_count($users)) { 92 | unset($form->errMsg, $form->note); 93 | $userList = []; 94 | foreach($users as $u) 95 | $userList[$u->user_id] = "$u->name [$u->user_id]"; 96 | // Order user list alphabetically 97 | asort($userList); 98 | $form->fieldsetLegend = EDIT_USER; 99 | if(userPermission() > PERMISSION_FULL) 100 | $form->submit = [EDIT_USER, DELETE_USER]; 101 | else 102 | $form->submit = EDIT_USER; 103 | $form->id = 'editUserForm'; 104 | $form->fields = ['Name [ID]' => ['select'=> ['user_id', $userList]]]; 105 | echo htmlForm($form); 106 | } 107 | } 108 | } 109 | 110 | h3("Form Notes"); 111 | echo '
    112 |
  • Name: Login username. Can be user\'s first name, first & last name/initials, callsign, 113 | or other name. Must be 2-24 alphanumeric characters. May contain spaces 114 |
  • Email: Optional. Not currently used but email features may be supported in the future 115 |
  • Password: Must be 6-16 printable ASCII characters 116 |
  • Location: Optional. User location eg. "Chicago, IL" or "Nottingham, UK" 117 |
  • Node #s: Optional. Space separated list of AllStar node numbers user owns or manages 118 |
  • Permission:
    119 |   • Read-Only: Cannot connect/disconnect nodes or add/delete favorites
    120 |   • Read/Modify: Can execute above functions only
    121 |   • Full: Can view Cfgs and add/edit < Full permission user accounts
    122 |   • Admin: Can edit Cfgs and add/edit < Admin permission user accounts
    123 |   • Superuser: Full access to all functions 124 |
  • Time Zone: User\'s Time Zone 125 |

126 | '; 127 | 128 | if(!empty($user)) 129 | return; 130 | 131 | h3("Initial Configuration Notes"); 132 | echo '
    133 |
  • AllScan defaults public (not logged-in) users to Read-Only access. This can be changed to 134 | None (no public access), Read/Modify, or to Full (no logins needed). To change 135 | this setting, Login, click the "Cfgs" link, and edit the "Public Permission" parameter. 136 |
  • If there will be more than one administrator it is recommended that each have a 137 | separate account with their name or callsign for the Name. 138 |
  • Additional user accounts can be created and edited on the "Users" page. 139 |
  • Your user account settings can be changed on the "Settings" page. 140 |
  • Do not lose your Login Name and Password. Admin passwords can be changed only by another 141 | Superuser or via SSH. 142 |

143 | '; 144 | 145 | showFooterLinks(); 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /user/index.php: -------------------------------------------------------------------------------- 1 | validate(); 35 | // If user not logged in, show Login Form 36 | if(empty($user) || !writeOk()) 37 | showLoginForm($_GET); // Does not return 38 | // Handle Logout 39 | if($user && isset($_GET['logout'])) 40 | processLogout(); // Does not return 41 | } 42 | 43 | pageInit(); 44 | if($userCnt) 45 | h1("User Administration"); 46 | 47 | // Process Add/Edit/Delete requests 48 | if(isset($_POST['Submit']) && (writeOk() || !$userCnt)) { 49 | $newUser = processForm($_POST); 50 | $userCnt = $userModel->getCount(); 51 | } 52 | if(empty($user) && $userCnt) 53 | redirect('user/'); 54 | 55 | if(!isset($newUser)) { 56 | $newUser = (object)['name'=>'', 'email'=>'', 'location'=>'', 'nodenums'=>[], 'timezone_id'=>0, 57 | 'permission'=>PERMISSION_READ_ONLY, 'edit'=>false]; 58 | } 59 | 60 | if(isset($newUser->confirmDelete)) { 61 | echo confirmForm('Confirm Delete User', 62 | htmlspecialchars("Permanently delete user $newUser->name [UserID $newUser->user_id]?"), 63 | ['user_id'=>$newUser->user_id, 'confirm'=>'1'], null, false, [DELETE_USER, 'Cancel']); 64 | asExit(); 65 | } 66 | 67 | // Show Users List 68 | if(!$newUser->edit && $userCnt) { 69 | h2('User Accounts'); 70 | showUsers($parms); 71 | } 72 | 73 | // Show Add / Edit forms 74 | if(!isset($userModel->error) && (writeOk() || !$userCnt)) 75 | $view->showForms($newUser); 76 | 77 | asExit(); 78 | 79 | //-------- functions -------- 80 | function showUsers($parms) { 81 | global $userModel, $view; 82 | // If Submit set clear parms to prevent sort links acting as forms 83 | if(isset($parms['Submit'])) 84 | $parms = []; 85 | // Sort by specified column & direction or default to last login DESC 86 | $sortCol = (isset($parms['sortCol']) && strlen($parms['sortCol']) == 1) ? $parms['sortCol'] : 'l'; 87 | $sortAsc = !((isset($parms['sortOrd']) && $parms['sortOrd'] === 'd') || 88 | (!isset($parms['sortOrd']) && $sortCol === 'l')); 89 | // Build ORDER BY clause. Use array to support ordering by multiple cols 90 | switch($sortCol) { 91 | case 'n': $order = ['n']; break; 92 | case 'e': $order = ['e']; break; 93 | case 'L': $order = ['L']; break; 94 | case 'p': $order = ['p']; break; 95 | case 't': $order = ['t']; break; 96 | default: $order = ['l']; break; 97 | } 98 | $ord = []; 99 | $n = 0; 100 | foreach($order as $n => $sc) { 101 | switch($sc) { 102 | case 'n': $ord[$n] = 'name'; break; 103 | case 'e': $ord[$n] = 'email'; break; 104 | case 'L': $ord[$n] = 'location'; break; 105 | case 'p': $ord[$n] = 'permission'; break; 106 | case 't': $ord[$n] = 'timezone_id'; break; 107 | default: $ord[$n] = 'last_login'; break; 108 | } 109 | if(!$sortAsc) 110 | $ord[$n] .= ' DESC'; 111 | $n++; 112 | } 113 | $orderBy = implode(',', $ord); 114 | // Get sorted list from DB 115 | $where = null; 116 | $users = $userModel->getUsers($where, $orderBy, PERMISSION_NONE); 117 | if(isset($userModel->error)) { 118 | errMsg($userModel->error); 119 | return; 120 | } 121 | if(!_count($users)) { 122 | h3("No Users found"); 123 | echo BR; 124 | return; 125 | } 126 | // Sort by name [n], email [e], location [L], last login [l] (default), timezone [t] 127 | $hdrCols = ['Name', 'Email', 'Location', 'Last Login', 'Permission', 'Time Zone']; 128 | if(count($users) > 1) { 129 | $colKey = ['n', 'e', 'L', 'l', 'p', 't']; 130 | foreach($hdrCols as $key => &$col) { 131 | $ck = $colKey[$key]; 132 | if($sortCol === $ck) { 133 | // Link to sort in opposite order 134 | $col = getSortLink($parms, $sortCol, !$sortAsc, $col); 135 | // Show an arrow indicating current sort col and direction 136 | $col .= upDownArrow($sortAsc); 137 | } else { 138 | // Link to sort in ASC order (or DESC order for login time) 139 | $col = getSortLink($parms, $ck, ($ck !== 'l'), $col); 140 | } 141 | } 142 | } 143 | $view->showUsers($users, $hdrCols[0], $hdrCols[1], $hdrCols[2], 'Node #s', $hdrCols[3], $hdrCols[4], $hdrCols[5]); 144 | } 145 | 146 | function processForm($parms) { 147 | global $userModel, $userCnt, $user; 148 | $Submit = $parms['Submit']; 149 | $newUser = arrayToObj($parms, ['user_id', 'name', 'email', 'location', 'nodenums', 'pass', 'permission', 'timezone_id']); 150 | if(!empty($user) && isset($newUser->user_id) && $newUser->user_id == $user->user_id) { 151 | p("Use Settings Page to edit your user account", 'error'); 152 | return; 153 | } 154 | $confirm = $_POST['confirm'] ?? null; 155 | $newUser->edit = ($Submit === EDIT_USER); 156 | $newUser->nodenums = isset($newUser->nodenums) ? parseIntList($newUser->nodenums) : []; 157 | if($Submit === DELETE_USER && $newUser->user_id) { 158 | $newUser = $userModel->getUserById($newUser->user_id, PERMISSION_NONE); 159 | if(!$newUser) { 160 | errMsg('User not found'); 161 | } elseif($confirm == 1) { 162 | $userLevel = userPermission(); 163 | $newUserLevel = userPermission($newUser); 164 | if(superUser() || (adminUser() && $userLevel > $newUserLevel)) { 165 | // Delete User 166 | if($userModel->delete($newUser)) 167 | return null; 168 | errMsg('Delete User Failed: ' . $userModel->error); 169 | unset($userModel->error); 170 | } else { 171 | p("Invalid permissions [$userLevel/$newUserLevel]", 'error'); 172 | } 173 | } else { 174 | $newUser->confirmDelete = true; 175 | } 176 | } 177 | // Process Add/Edit Request 178 | if($Submit === ADD_USER || ($newUser->edit && !empty($newUser->name))) { 179 | // Get existing user data if edit request 180 | if($newUser->edit) { 181 | $user0 = $userModel->getUserById($newUser->user_id); 182 | if(!isset($newUser->permission)) 183 | $newUserLevel = userPermission($user0); 184 | } 185 | // Verify user has permission to add/edit this user 186 | $userLevel = userPermission(); 187 | if(!isset($newUserLevel)) 188 | $newUserLevel = userPermission($newUser); 189 | if($userCnt && ($newUser->permission < PERMISSION_NONE || $newUser->permission > $userLevel || 190 | (!superUser() && $newUser->permission >= $userLevel))) 191 | $newUser->errMsg = "Invalid permissions [$userLevel/$newUserLevel]"; 192 | elseif(!$newUser->edit && $newUser->permission <= PERMISSION_NONE) 193 | $newUser->errMsg = 'Cannot create new user with no permissions'; 194 | elseif($newUser->edit && !$user0) 195 | $newUser->errMsg = 'User not found'; 196 | elseif(!$userModel->validateFields($newUser)) { 197 | $newUser->errMsg = $userModel->error; 198 | unset($userModel->error); 199 | } elseif(!$newUser->edit && !$userModel->validatePassword($newUser->pass)) { 200 | $newUser->errMsg = $userModel->error; 201 | unset($userModel->error); 202 | } elseif($newUser->edit) { 203 | // Below can return 0 [rows affected] if user submits form with no changes 204 | if($userModel->update($newUser) === null) { 205 | errMsg('Edit User Failed: ' . $userModel->error); 206 | unset($userModel->error); 207 | } else { 208 | okMsg('Edit User Successful'); 209 | unset($newUser); 210 | } 211 | } else { 212 | $ret = $userModel->add($newUser); 213 | if(!$ret) { 214 | errMsg('Add User Failed: ' . $userModel->error); 215 | unset($userModel->error); 216 | } else { 217 | okMsg('Add User Successful'); 218 | unset($newUser); 219 | } 220 | } 221 | } 222 | return $newUser; 223 | } 224 | 225 | function showLoginForm($parms) { 226 | global $html; 227 | pageInit('', false); 228 | $form = new stdClass(); 229 | $form->submit = $form->fieldsetLegend = 'Log In'; 230 | $form->fields = []; 231 | $form->fields['User Name / Call Sign'] = ['text' => ['name', $parms->name ?? '']]; 232 | $form->fields['Password'] = ['password' => ['pass']]; 233 | $form->fields['Remember Me'] = ['checkbox' => ['remember']]; 234 | echo htmlForm($form) . BR; 235 | asExit(); 236 | } 237 | 238 | function processLogin($parms) { 239 | global $userModel; 240 | if(!isset($parms['remember'])) 241 | $parms['remember'] = false; 242 | $loginOK = $userModel->validateLogin($parms['name'], $parms['pass'], $parms['remember']); 243 | if($loginOK && !isset($userModel->error)) 244 | redirect(); 245 | // else login failed 246 | sleep(2); // Slow crack attempts 247 | if(isset($userModel->error)) { 248 | $msg = "Login Error: $userModel->error"; 249 | } else { 250 | $msg = "Login Failed: Invalid Name / Call Sign / Password"; 251 | } 252 | pageInit('', false); 253 | h1('Login Results'); 254 | p($msg, 'error'); 255 | asExit(); 256 | } 257 | 258 | function processLogout() { 259 | global $userModel; 260 | $userModel->logout(); 261 | redirect(); 262 | } 263 | -------------------------------------------------------------------------------- /user/settings/index.php: -------------------------------------------------------------------------------- 1 | validate(); 21 | if(!readOk()) 22 | redirect('user/'); 23 | 24 | define('UPDATE_SETTINGS', 'Update Settings'); 25 | define('CHANGE_PASSWORD', 'Change Password'); 26 | 27 | $post = arrayToObj($_POST, ['name', 'email', 'location', 'nodenums', 'timezone_id', 'pass', 'confirm']); 28 | $post->nodenums = isset($post->nodenums) ? parseIntList($post->nodenums) : []; 29 | $Submit = $_POST['Submit'] ?? null; 30 | 31 | pageInit(); 32 | 33 | h1('User Account Settings'); 34 | 35 | if(isset($userModel->error)) 36 | errMsg($userModel->error); 37 | 38 | switch($Submit) { 39 | case UPDATE_SETTINGS: 40 | if(!$userModel->validateFields($post)) { 41 | $post->errMsg = $userModel->error; 42 | unset($userModel->error); 43 | } else { 44 | $newUser = $user; 45 | $newUser->name = $post->name; 46 | $newUser->email = $post->email; 47 | $newUser->location = $post->location; 48 | $newUser->nodenums = $post->nodenums; 49 | $newUser->timezone_id = $post->timezone_id; 50 | // Below can return 0 [rows affected] if user submits form with no changes 51 | if($userModel->update($newUser) === null) { 52 | errMsg('Edit User Failed: ' . $userModel->error); 53 | unset($userModel->error); 54 | } else { 55 | okMsg('Edit User Successful'); 56 | unset($post); 57 | // Read in user from DB 58 | $user = $userModel->getUserById($user->user_id); 59 | } 60 | } 61 | break; 62 | case CHANGE_PASSWORD: 63 | if(!$userModel->validatePassword($post->pass)) { 64 | $post->errMsg2 = $userModel->error; 65 | unset($userModel->error); 66 | } elseif(!$userModel->changePassword($_POST)) { 67 | $post->errMsg2 = $userModel->error; 68 | } else { 69 | okMsg("Change Password Successful"); 70 | } 71 | break; 72 | } 73 | 74 | $form = new stdClass(); 75 | $form->class = 'left'; 76 | if(isset($post->errMsg)) 77 | $form->errMsg = $post->errMsg; 78 | $form->id = 'editUserForm'; 79 | $permList = $userModel->getPermissionList(userPermission(), userPermission()); 80 | $form->fields = [ 81 | 'Name' => ['text' => ['name', $user->name ?? '']], 82 | 'Email' => ['text' => ['email', $user->email ?? '']], 83 | 'Location' => ['text' => ['location', $user->location ?? '']], 84 | 'Node #s' => ['text' => ['nodenums', implode(' ', $user->nodenums ?? '')]], 85 | 'Permission' => ['select' => ['permission', $permList, userPermission()]], 86 | 'Time Zone' => ['select' => ['timezone_id', $timezoneDef, $user->timezone_id]]]; 87 | $form->submit = $form->fieldsetLegend = UPDATE_SETTINGS; 88 | echo htmlForm($form) . BR; 89 | 90 | unset($form->errMsg); 91 | if(isset($post->errMsg2)) 92 | $form->errMsg = $post->errMsg2; 93 | $form->id = 'changePassForm'; 94 | $form->fields = [ 95 | 'New Password' => ['text' => ['pass']], 96 | 'Confirm New Password' => ['text' => ['confirm']]]; 97 | $form->submit = $form->fieldsetLegend = CHANGE_PASSWORD; 98 | echo htmlForm($form) . BR; 99 | 100 | h3("Form Notes:"); 101 | echo '
    102 |
  • Name: Login username. Can be your first name, first & last name/initials, your callsign,
    103 | or other name. Must be 2-24 alphanumeric characters. May contain spaces 104 |
  • Email: Optional. Not currently used but email features may be supported in the future 105 |
  • Location: Optional. Your location eg. "Chicago, IL" or "Nottingham, UK" 106 |
  • Node #s: Optional. Space separated list of AllStar node numbers you own or manage 107 |
  • Time Zone: Default is UTC 108 |
  • Password: Must be 6-16 printable ASCII characters 109 |

110 | '; 111 | 112 | asExit(); 113 | --------------------------------------------------------------------------------