Merge remote-tracking branch 'upstream/dev' into cache_buster

This commit is contained in:
HB9HIL
2026-02-13 16:39:03 +01:00
676 changed files with 464554 additions and 150752 deletions

View File

@@ -37,6 +37,7 @@ class AdifHelper {
'EQSL_QSL_RCVD',
'EQSL_QSL_SENT',
'EQSL_STATUS',
'EQSL_AG',
'FISTS',
'FISTS_CC',
'FORCE_INIT',
@@ -106,6 +107,8 @@ class AdifHelper {
'MORSE_KEY_INFO',
'MORSE_KEY_TYPE',
'QSLMSG_RCVD',
'DCL_QSL_RCVD',
'DCL_QSL_SENT'
);
$dateFields = array(
@@ -119,7 +122,7 @@ class AdifHelper {
'HRDLOG_QSO_UPLOAD_DATE',
'QRZCOM_QSO_UPLOAD_DATE',
'DCL_QSLRDATE',
'DCL_QSLSDATE',
'DCL_QSLSDATE'
);
/**
@@ -259,6 +262,15 @@ class AdifHelper {
return $line;
}
function getAdifHeader($app_name,$version) {
$adif_header = "Wavelog ADIF export\n";
$adif_header .= "<ADIF_VER:5>3.1.6\n";
$adif_header .= "<PROGRAMID:".strlen($app_name).">".$app_name."\r\n";
$adif_header .= "<PROGRAMVERSION:".strlen($version).">".$version."\r\n";
$adif_header .= "<EOH>\n\n";
return $adif_header;
}
function getAdifFieldLine($adifcolumn, $dbvalue) {
if ($dbvalue !== "" && $dbvalue !== null && $dbvalue !== 0) {
return "<" . $adifcolumn . ":" . mb_strlen($dbvalue, "UTF-8") . ">" . $dbvalue . "\r\n";

View File

@@ -138,62 +138,75 @@ class ADIF_Parser
//the following function does the processing of the array into its key and value pairs
public function record_to_array($record)
{
$return = array();
for($a = 0; $a < mb_strlen($record, "UTF-8"); $a++)
{
if(mb_substr($record, $a, 1, "UTF-8") == '<') //find the start of the tag
{
$tag_name = "";
$value = "";
$len_str = "";
$len = 0;
$a++; //go past the <
while((mb_substr($record, $a, 1, "UTF-8") != ':') && ($a<=mb_strlen($record, "UTF-8"))) //get the tag
{
$tag_name = $tag_name.mb_substr($record, $a, 1, "UTF-8"); //append this char to the tag name
$a++;
// Prevent iterating $a past record length
if ($a == mb_strlen($record, "UTF-8")) {
return;
}
};
$a++; //iterate past the colon
while(mb_substr($record, $a, 1, "UTF-8") != '>' && mb_substr($record, $a, 1, "UTF-8") != ':' && ($a<=mb_strlen($record, "UTF-8")))
{
$len_str = $len_str.mb_substr($record, $a, 1, "UTF-8");
$a++;
};
if(mb_substr($record, $a, 1, "UTF-8") == ':')
{
while((mb_substr($record, $a, 1, "UTF-8") != '>') && ($a<=mb_strlen($record, "UTF-8")))
{
$a++;
};
};
$len = (int)$len_str;
$a++;
{
$return = array();
$length = mb_strlen($record, "UTF-8");
$value = mb_substr($record, $a, $len, "UTF-8");
$a = $a + $len - 1;
$return[mb_strtolower($tag_name, "UTF-8")] = $value;
};
//skip comments
if(mb_substr($record, $a, 1, "UTF-8") == "#")
{
while($a < mb_strlen($record, "UTF-8"))
{
if(mb_substr($record, $a, 1, "UTF-8") == "\n")
{
break;
}
for ($a = 0; $a < $length; $a++) {
if (mb_substr($record, $a, 1, "UTF-8") == '<') {
$tag_name = "";
$value = "";
$len_str = "";
$len = 0;
$a++; //go past the <
while ($a < $length && mb_substr($record, $a, 1, "UTF-8") != ':') { //get the tag
$tag_name .= mb_substr($record, $a, 1, "UTF-8");
$a++;
}
// If we reached the end unexpectedly, exit
if ($a >= $length) break;
$a++; //iterate past the colon
// Get length string
while ($a < $length && mb_substr($record, $a, 1, "UTF-8") != '>' && mb_substr($record, $a, 1, "UTF-8") != ':') {
$len_str .= mb_substr($record, $a, 1, "UTF-8");
$a++;
}
// If malformed tag or no length string, skip this tag
if (!ctype_digit($len_str)) {
continue;
}
$len = (int)$len_str;
// Skip extra colon if present
if ($a < $length && mb_substr($record, $a, 1, "UTF-8") == ':') {
while ($a < $length && mb_substr($record, $a, 1, "UTF-8") != '>') {
$a++;
}
}
};
return $return;
$a++; // past >
// Validate there are enough characters left
if (($a + $len) > $length) {
// Not enough characters for the value; skip this tag
break;
}
$value = mb_substr($record, $a, $len, "UTF-8");
$a += $len - 1; // adjust for loop increment
$return[mb_strtolower($tag_name, "UTF-8")] = $value;
}
// skip comments
if (mb_substr($record, $a, 1, "UTF-8") == "#") {
while ($a < $length) {
if (mb_substr($record, $a, 1, "UTF-8") == "\n") {
break;
}
$a++;
}
}
}
return $return;
}
//finds the next record in the file
public function get_record()
{

View File

@@ -147,7 +147,7 @@ class Cabrilloformat {
}
if ($grid_export == true) {
$returnstring .= substr($qso->station_gridsquare, 0, 4) ?? '' ." ";
$returnstring .= (substr($qso->station_gridsquare, 0, 4) ?? '') ." ";
}
if ($qso->COL_STX_STRING != "") {
@@ -161,7 +161,7 @@ class Cabrilloformat {
}
if ($grid_export == true) {
$returnstring .= substr($qso->COL_GRIDSQUARE, 0, 4) ?? '' ." ";
$returnstring .= (substr($qso->COL_GRIDSQUARE, 0, 4) ?? '') ." ";
}
if ($qso->COL_SRX_STRING != "") {

View File

@@ -18,171 +18,243 @@ class Callbook {
// Implement the following:
// - Implement callsign reduced logic
public function getCallbookData($callsign) {
switch ($this->ci->config->item('callbook')) {
case 'qrz':
if ($this->ci->config->item('qrz_username') == null || $this->ci->config->item('qrz_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
return $callbook;
// Load callbook configuration from config.php
$source_callbooks = $this->ci->config->item('callbook');
$callbook_errors = array();
// Check if the source callbook is a single element or an array
if (is_array($source_callbooks)) {
// Parse each callbook in the array until we get a valid result
foreach ($source_callbooks as $source) {
$callbook = $this->queryCallbook($callsign, $source);
if (!isset($callbook['error']) || $callbook['error'] == '') {
break;
} else {
$callbook_errors['error_'.$source] = $callbook['error'];
$callbook_errors['error_'.$source.'_name'] = $callbook['source'];
}
return $this->qrz($this->ci->config->item('qrz_username'), $this->ci->config->item('qrz_password'), $callsign, $this->ci->config->item('use_fullname'));
}
} else {
// Single callbook lookup (default behavior)
$callbook = $this->queryCallbook($callsign, $source_callbooks);
}
// Handle callbook specific fields
if (! array_key_exists('geoloc', $callbook)) {
$callbook['geoloc'] = '';
}
// qrz.com gives AA00aa if the user deleted his grid from the profile
$this->ci->load->library('qra');
if (!array_key_exists('gridsquare', $callbook) || !$this->ci->qra->validate_grid($callbook['gridsquare'])) {
$callbook['gridsquare'] = '';
}
if (isset($callbook['error']) && $callbook['error'] != '') {
if (is_array($source_callbooks)) {
foreach ($source_callbooks as $source) {
if (isset($callbook_errors['error_'.$source])) {
$callbook['error_'.$source] = $callbook_errors['error_'.$source];
$callbook['error_'.$source.'_name'] = $callbook_errors['error_'.$source.'_name'];
}
}
}
}
return $callbook;
}
function queryCallbook($callsign, $source) {
switch ($source) {
case 'qrz':
$callbook = $this->qrz($callsign, $this->ci->config->item('use_fullname'));
break;
case 'qrzcq':
if ($this->ci->config->item('qrzcq_username') == null || $this->ci->config->item('qrzcq_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
return $callbook;
}
return $this->qrzcq($this->ci->config->item('qrzcq_username'), $this->ci->config->item('qrzcq_password'), $callsign);
$callbook = $this->qrzcq($callsign);
break;
case 'hamqth':
if ($this->ci->config->item('hamqth_username') == null || $this->ci->config->item('hamqth_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
return $callbook;
}
return $this->hamqth($this->ci->config->item('hamqth_username'), $this->ci->config->item('hamqth_password'), $callsign);
$callbook = $this->hamqth($callsign);
break;
case 'qrzru':
if ($this->ci->config->item('qrzru_username') == null || $this->ci->config->item('qrzru_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
return $callbook;
}
return $this->qrzru($this->ci->config->item('qrzru_username'), $this->ci->config->item('qrzru_password'), $callsign);
$callbook = $this->qrzru($callsign);
break;
default:
$callbook['error'] = 'No callbook defined. Please review configuration.';
return $callbook;
}
log_message('debug', 'Callbook lookup for '.$callsign.' using '.$source.': '.((($callbook['error'] ?? '' ) != '') ? $callbook['error'] : 'Success'));
return $callbook;
}
function qrz($username, $password, $callsign, $fullname) {
function qrz($callsign, $fullname) {
if (!$this->ci->load->is_loaded('qrz')) {
$this->ci->load->library('qrz');
}
if ($this->ci->config->item('qrz_username') == null || $this->ci->config->item('qrz_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
$callbook['source'] = $this->ci->qrz->sourcename();
} else {
$username = $this->ci->config->item('qrz_username');
$password = $this->ci->config->item('qrz_password');
if (!$this->ci->session->userdata('qrz_session_key')) {
$qrz_session_key = $this->ci->qrz->session($username, $password);
$this->ci->session->set_userdata('qrz_session_key', $qrz_session_key);
}
if (!$this->ci->session->userdata('qrz_session_key')) {
$qrz_session_key = $this->ci->qrz->session($username, $password);
$this->ci->session->set_userdata('qrz_session_key', $qrz_session_key);
}
$callbook = $this->ci->qrz->search($callsign, $this->ci->session->userdata('qrz_session_key'), $fullname);
if ($callbook['error'] ?? '' == 'Invalid session key') {
$qrz_session_key = $this->ci->qrz->session($username, $password);
$this->ci->session->set_userdata('qrz_session_key', $qrz_session_key);
$callbook = $this->ci->qrz->search($callsign, $this->ci->session->userdata('qrz_session_key'), $fullname);
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrz->search($plaincall, $this->ci->session->userdata('qrz_session_key'), $fullname, true);
if ($callbook['error'] ?? '' == 'Invalid session key') {
$qrz_session_key = $this->ci->qrz->session($username, $password);
$this->ci->session->set_userdata('qrz_session_key', $qrz_session_key);
$callbook = $this->ci->qrz->search($callsign, $this->ci->session->userdata('qrz_session_key'), $fullname);
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrz->search($plaincall, $this->ci->session->userdata('qrz_session_key'), $fullname, true);
}
}
$callbook['source'] = $this->ci->qrz->sourcename();
return $callbook;
}
function qrzcq($username, $password, $callsign) {
function qrzcq($callsign) {
if (!$this->ci->load->is_loaded('qrzcq')) {
$this->ci->load->library('qrzcq');
}
if ($this->ci->config->item('qrzcq_username') == null || $this->ci->config->item('qrzcq_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
$callbook['source'] = $this->ci->qrzcq->sourcename();
} else {
$username = $this->ci->config->item('qrzcq_username');
$password = $this->ci->config->item('qrzcq_password');
if (!$this->ci->session->userdata('qrzcq_session_key')) {
$result = $this->ci->qrzcq->session($username, $password);
if ($result[0] == 0) {
$this->ci->session->set_userdata('qrzcq_session_key', $result[1]);
} else {
$data['error'] = __("QRZCQ Error").": ".$result[1];
return $data;
if (!$this->ci->session->userdata('qrzcq_session_key')) {
$result = $this->ci->qrzcq->session($username, $password);
if ($result[0] == 0) {
$this->ci->session->set_userdata('qrzcq_session_key', $result[1]);
} else {
$data['error'] = __("QRZCQ Error").": ".$result[1];
$data['source'] = $this->ci->qrzcq->sourcename();
return $data;
}
}
$callbook = $this->ci->qrzcq->search($callsign, $this->ci->session->userdata('qrzcq_session_key'));
if ($callbook['error'] ?? '' == 'Invalid session key') {
$qrzcq_session_key = $this->ci->qrzcq->session($username, $password);
$this->ci->session->set_userdata('qrzcq_session_key', $qrzcq_session_key);
$callbook = $this->ci->qrzcq->search($callsign, $this->ci->session->userdata('qrzcq_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrzcq->search($plaincall, $this->ci->session->userdata('qrzcq_session_key'), true);
}
}
$callbook = $this->ci->qrzcq->search($callsign, $this->ci->session->userdata('qrzcq_session_key'));
if ($callbook['error'] ?? '' == 'Invalid session key') {
$qrzcq_session_key = $this->ci->qrzcq->session($username, $password);
$this->ci->session->set_userdata('qrzcq_session_key', $qrzcq_session_key);
$callbook = $this->ci->qrzcq->search($callsign, $this->ci->session->userdata('qrzcq_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrzcq->search($plaincall, $this->ci->session->userdata('qrzcq_session_key'), true);
}
$callbook['source'] = $this->ci->qrzcq->sourcename();
return $callbook;
}
function hamqth($username, $password, $callsign) {
function hamqth($callsign) {
// Load the HamQTH library
if (!$this->ci->load->is_loaded('hamqth')) {
$this->ci->load->library('hamqth');
}
if ($this->ci->config->item('hamqth_username') == null || $this->ci->config->item('hamqth_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
$callbook['source'] = $this->ci->hamqth->sourcename();
} else {
$username = $this->ci->config->item('hamqth_username');
$password = $this->ci->config->item('hamqth_password');
if (!$this->ci->session->userdata('hamqth_session_key')) {
$hamqth_session_key = $this->ci->hamqth->session($username, $password);
$this->ci->session->set_userdata('hamqth_session_key', $hamqth_session_key);
}
if (!$this->ci->session->userdata('hamqth_session_key')) {
$hamqth_session_key = $this->ci->hamqth->session($username, $password);
if ($hamqth_session_key == false) {
$callbook['error'] = __("Error obtaining a session key for HamQTH query");
$callbook['source'] = $this->ci->hamqth->sourcename();
return $callbook;
} else {
$this->ci->session->set_userdata('hamqth_session_key', $hamqth_session_key);
}
}
$callbook = $this->ci->hamqth->search($callsign, $this->ci->session->userdata('hamqth_session_key'));
// If HamQTH session has expired, start a new session and retry the search.
if ($callbook['error'] == "Session does not exist or expired") {
$hamqth_session_key = $this->ci->hamqth->session($username, $password);
$this->ci->session->set_userdata('hamqth_session_key', $hamqth_session_key);
$callbook = $this->ci->hamqth->search($callsign, $this->ci->session->userdata('hamqth_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->hamqth->search($plaincall, $this->ci->session->userdata('hamqth_session_key'), true);
// If HamQTH session has expired, start a new session and retry the search.
if ($callbook['error'] == "Session does not exist or expired") {
$hamqth_session_key = $this->ci->hamqth->session($username, $password);
$this->ci->session->set_userdata('hamqth_session_key', $hamqth_session_key);
$callbook = $this->ci->hamqth->search($callsign, $this->ci->session->userdata('hamqth_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->hamqth->search($plaincall, $this->ci->session->userdata('hamqth_session_key'), true);
}
}
$callbook['source'] = $this->ci->hamqth->sourcename();
return $callbook;
}
function qrzru($username, $password, $callsign) {
function qrzru($callsign) {
if (!$this->ci->load->is_loaded('qrzru')) {
$this->ci->load->library('qrzru');
}
if ($this->ci->config->item('qrzru_username') == null || $this->ci->config->item('qrzru_password') == null) {
$callbook['error'] = 'Lookup not configured. Please review configuration.';
$callbook['source'] = $this->ci->qrzru->sourcename();
} else {
$username = $this->ci->config->item('qrzru_username');
$password = $this->ci->config->item('qrzru_password');
if (!$this->ci->session->userdata('qrzru_session_key')) {
$result = $this->ci->qrzru->session($username, $password);
$this->ci->session->set_userdata('qrzru_session_key', $result);
}
if (!$this->ci->session->userdata('qrzru_session_key')) {
$result = $this->ci->qrzru->session($username, $password);
$this->ci->session->set_userdata('qrzru_session_key', $result);
}
$callbook = $this->ci->qrzru->search($callsign, $this->ci->session->userdata('qrzru_session_key'));
if ($callbook['error'] ?? '' == 'Session does not exist or expired') {
$qrzru_session_key = $this->ci->qrzru->session($username, $password);
$this->ci->session->set_userdata('qrzru_session_key', $qrzru_session_key);
$callbook = $this->ci->qrzru->search($callsign, $this->ci->session->userdata('qrzru_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Callsign not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrzru->search($plaincall, $this->ci->session->userdata('qrzru_session_key'), true);
if ($callbook['error'] ?? '' == 'Session does not exist or expired') {
$qrzru_session_key = $this->ci->qrzru->session($username, $password);
$this->ci->session->set_userdata('qrzru_session_key', $qrzru_session_key);
$callbook = $this->ci->qrzru->search($callsign, $this->ci->session->userdata('qrzru_session_key'));
}
if (strpos($callbook['error'] ?? '', 'Callsign not found') !== false && strpos($callsign, "/") !== false) {
$plaincall = $this->get_plaincall($callsign);
// Now try again but give back reduced data, as we can't validate location and stuff (true at the end)
$callbook = $this->ci->qrzru->search($plaincall, $this->ci->session->userdata('qrzru_session_key'), true);
}
}
$callbook['source'] = $this->ci->qrzru->sourcename();
return $callbook;
}
function get_plaincall($callsign) {
$split_callsign = explode('/', $callsign);
if (count($split_callsign) == 1) { // case F0ABC --> return cel 0 //
$lookupcall = $split_callsign[0];
} else if (count($split_callsign) == 3) { // case EA/F0ABC/P --> return cel 1 //
$lookupcall = $split_callsign[1];
} else { // case F0ABC/P --> return cel 0 OR case EA/FOABC --> retunr 1 (normaly not exist) //
if (in_array(strtoupper($split_callsign[1]), array('P', 'M', 'MM', 'QRP', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))) {
$lookupcall = $split_callsign[0];
} else if (strlen($split_callsign[1]) > 3) { // Last Element longer than 3 chars? Take that as call
$lookupcall = $split_callsign[1];
} else { // Last Element up to 3 Chars? Take first element as Call
$lookupcall = $split_callsign[0];
}
if (count($split_callsign) == 1) { // case of plain callsign --> return callsign
return $callsign;
}
return $lookupcall;
// Case of known suffixes that are not part of the callsign
if (in_array(strtoupper($split_callsign[1]), array('LGT', 'AM', 'LH', 'A', 'B', 'R', 'T', 'X', 'D', 'P', 'M', 'MM', 'QRP', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))) {
return $split_callsign[0];
}
// case EA/FOABC --> return 1
if (strlen($split_callsign[1]) > 3) { // Last Element longer than 3 chars? Take that as call
return $split_callsign[1];
}
// case F0ABC/KH6 --> return cell 0
return $split_callsign[0];
}
}

View File

@@ -1,13 +1,13 @@
<?php
class CBR_Parser
{
public function parse_from_file($filename, $serial_number_present = false) : array
public function parse_from_file($filename, $serial_number_present = false, $trx_number_present = false) : array
{
//load file, call parser
return $this->parse(mb_convert_encoding(file_get_contents($filename), "UTF-8"), $serial_number_present);
return $this->parse(mb_convert_encoding(file_get_contents($filename), "UTF-8"), $serial_number_present, $trx_number_present);
}
public function parse(string $input, $serial_number_present = false) : array
public function parse(string $input, $serial_number_present = false, $trx_number_present = false) : array
{
//split the input into lines
$lines = explode("\n", trim($input));
@@ -35,6 +35,11 @@ class CBR_Parser
$qso_mode = false;
}
//if we encounter a QTC, skip that line
if(strpos($line, 'QTC:') === 0){
continue;
}
//if we encounter "END-OF-LOG", stop processing lines
if (strpos($line, 'END-OF-LOG') === 0) {
break;
@@ -59,13 +64,38 @@ class CBR_Parser
//split the line into the elements
$qso_elements = preg_split('/\s+/', trim($line));
//check occurances of signal rapport values
$counts = array_count_values($qso_elements);
$count59s = ($counts["59"] ?? 0) + ($counts["599"] ?? 0);
//for those few contests who do not have signal exchange, synthesize one based on best efforts
if($count59s < 2 and isset($header['CALLSIGN'])){
//get own callsign from header
$own_callsign = $header['CALLSIGN'];
//get position of own callsign
$index = array_search($own_callsign, $qso_elements);
//add synthesized sent signal rapport after own callsign
if ($index !== false) {
array_splice($qso_elements, $index + 1, 0, "59");
}
//search for the next amateur radio callsign after my own and add another 59. Abort after first find
for ($i = $index + 1; $i < count($qso_elements); $i++) {
$value = $qso_elements[$i];
if(preg_match('/(?=[A-Z0-9]*[A-Z])[A-Z0-9]{1,3}[0-9][A-Z0-9]{0,7}/', $value) === 1){
array_splice($qso_elements, $i + 1, 0, "59");
break;
}
}
}
//determine maximum qso field size
$max_qso_fields = max($max_qso_fields, count($qso_elements));
$max_qso_fields = max($max_qso_fields ?? 0, count($qso_elements));
//add qso elements to qso line array
array_push($qso_lines_raw, $qso_elements);
//find all occurrences of "59"
//find all occurrences of "59" or "599"
$indices_of_59 = [];
foreach ($qso_elements as $index => $value) {
if ($value === "59" or $value === "599") {
@@ -73,6 +103,15 @@ class CBR_Parser
}
}
//abort further processing if we find a line without a valid signal report (59 or 599)
if(count($indices_of_59) < 1) {
//return error result
$result = [];
$result['error'] = __("Broken CBR file - no valid exchange or callsigns found");
return $result;
}
//find common indices position
if ($common_59_indices === null) {
//initialize common indices on the first iteration
@@ -82,6 +121,9 @@ class CBR_Parser
$common_59_indices = array_intersect($common_59_indices, $indices_of_59);
}
//add qso elements to qso line array
array_push($qso_lines_raw, $qso_elements);
//skip to next line
continue;
}
@@ -89,30 +131,20 @@ class CBR_Parser
//abort further processing if no qso lines were found, return header only
if(count($qso_lines_raw) < 1) {
$result = [];
$result["HEADER"] = $header;
$result["QSOS"] = [];
$result["SENT_59_POS"] = 0;
$result["RCVD_59_POS"] = 0;
$result["SENT_EXCHANGE_COUNT"] = 0;
$result["RCVD_EXCHANGE_COUNT"] = 0;
//return result
//return error result
$result = [];
$result['error'] = __("Broken CBR file - no QSO data found.");
return $result;
}
//abort if basic things (Callsign and Contest ID) are not included in the header
$header_fields = array_keys($header);
if(!in_array('CALLSIGN', $header_fields) or !in_array('CONTEST', $header_fields)){
//return error result
$result = [];
$result["HEADER"] = $header;
$result["QSOS"] = [];
$result["SENT_59_POS"] = 0;
$result["RCVD_59_POS"] = 0;
$result["SENT_EXCHANGE_COUNT"] = 0;
$result["RCVD_EXCHANGE_COUNT"] = 0;
//return blank result
$result['error'] = __("Broken CBR file - incomplete header found.");
return $result;
}
@@ -120,6 +152,15 @@ class CBR_Parser
$sent_59_pos = min($common_59_indices);
$rcvd_59_pos = max($common_59_indices);
//abort if position of sent and received signal exchange is identical
if($sent_59_pos == $rcvd_59_pos){
//return error result
$result = [];
$result['error'] = __("Broken CBR file - no valid exchange or callsigns found");
return $result;
}
//get codeigniter instance
$CI = &get_instance();
@@ -171,7 +212,7 @@ class CBR_Parser
//get all remaining received exchanges
$exchange_nr = 1;
$startindex = ($rcvd_59_pos + ($serial_number_present ? 2 : 1));
$endindex = (count($line));
$endindex = count($line) - ($trx_number_present ? 1 : 0);
for ($i = $startindex; $i < $endindex; $i++) {
$qso_line["RCVD_EXCH_" . $exchange_nr] = $line[$i];
$exchange_nr++;
@@ -200,12 +241,15 @@ class CBR_Parser
case '144':
$band = '2m';
break;
case '220':
case '222':
$band = '1.25m';
break;
case '430':
case '432':
$band = '70cm';
break;
case '900':
case '902':
$band = '33cm';
break;
@@ -292,7 +336,7 @@ class CBR_Parser
$result["SENT_59_POS"] = $sent_59_pos;
$result["RCVD_59_POS"] = $rcvd_59_pos;
$result["SENT_EXCHANGE_COUNT"] = $rcvd_59_pos - $sent_59_pos - ($serial_number_present ? 3 : 2);
$result["RCVD_EXCHANGE_COUNT"] = $max_qso_fields - 1 - $rcvd_59_pos - ($serial_number_present ? 1 : 0);
$result["RCVD_EXCHANGE_COUNT"] = $max_qso_fields - 1 - $rcvd_59_pos - ($serial_number_present ? 1 : 0) - ($trx_number_present ? 1 : 0);
//return result
return $result;

View File

@@ -100,7 +100,6 @@ class Curl {
// Add the filepath
$url .= $file_path;
$this->option(CURLOPT_BINARYTRANSFER, TRUE);
$this->option(CURLOPT_VERBOSE, TRUE);
return $this->execute();
@@ -304,7 +303,6 @@ class Curl {
$this->error_code = curl_errno($this->session);
$this->error_string = curl_error($this->session);
curl_close($this->session);
$this->set_defaults();
return FALSE;
@@ -313,7 +311,6 @@ class Curl {
// Request successful
else
{
curl_close($this->session);
$response = $this->response;
$this->set_defaults();
return $response;

View File

@@ -0,0 +1,125 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
use Wavelog\Dxcc\Dxcc;
require_once APPPATH . '../src/Dxcc/Dxcc.php';
/**
* DXCluster Cache Library
* Centralizes cache key generation and invalidation for DXCluster features.
*/
class DxclusterCache {
protected $CI;
public function __construct() {
$this->CI =& get_instance();
}
// =========================================================================
// CACHE KEY GENERATION
// =========================================================================
/**
* Generate RAW spot cache key (instance-wide, shared by all users)
*/
public function get_raw_cache_key($maxage, $band) {
return "dxcluster_raw_{$maxage}_{$band}_Any_All";
}
/**
* Generate logbook IDs key component (user-specific)
*/
public function get_logbook_key($user_id, $logbook_ids, $confirmation_prefs) {
$logbook_ids_str = implode('_', $logbook_ids);
$confirmation_hash = md5($confirmation_prefs);
return "{$user_id}_{$logbook_ids_str}_{$confirmation_hash}";
}
/**
* Generate WORKED callsign cache key
*/
public function get_worked_call_key($logbook_key, $callsign) {
return "dxcluster_worked_call_{$logbook_key}_{$callsign}";
}
/**
* Generate WORKED DXCC cache key
*/
public function get_worked_dxcc_key($logbook_key, $dxcc) {
return "dxcluster_worked_dxcc_{$logbook_key}_{$dxcc}";
}
/**
* Generate WORKED continent cache key
*/
public function get_worked_cont_key($logbook_key, $cont) {
return "dxcluster_worked_cont_{$logbook_key}_{$cont}";
}
// =========================================================================
// CACHE INVALIDATION
// =========================================================================
/**
* Invalidate cache after QSO add/edit/delete for current user
* @param string $callsign - The worked callsign
*/
public function invalidate_for_callsign($callsign) {
// Skip if worked cache is disabled
if ($this->CI->config->item('enable_dxcluster_file_cache_worked') !== true) return;
if (empty($callsign)) return;
// Get current user's logbook key
$logbook_key = $this->_get_current_user_logbook_key();
if (empty($logbook_key)) return;
// Delete callsign cache
$this->_delete_from_cache($this->get_worked_call_key($logbook_key, $callsign));
// Look up DXCC and continent from callsign
$dxccobj = new Dxcc(null);
$dxcc_info = $dxccobj->dxcc_lookup($callsign, date('Y-m-d'));
if (!empty($dxcc_info['adif'])) {
$this->_delete_from_cache($this->get_worked_dxcc_key($logbook_key, $dxcc_info['adif']));
}
if (!empty($dxcc_info['cont'])) {
$this->_delete_from_cache($this->get_worked_cont_key($logbook_key, $dxcc_info['cont']));
}
}
/**
* Get current user's logbook key from session
*/
private function _get_current_user_logbook_key() {
$user_id = $this->CI->session->userdata('user_id');
$active_logbook = $this->CI->session->userdata('active_station_logbook');
if (empty($user_id) || empty($active_logbook)) return null;
$this->CI->load->model('logbooks_model');
$logbook_ids = $this->CI->logbooks_model->list_logbook_relationships($active_logbook);
$confirmation_prefs = $this->CI->session->userdata('user_default_confirmation') ?? '';
if (empty($logbook_ids)) return null;
return $this->get_logbook_key($user_id, $logbook_ids, $confirmation_prefs);
}
// =========================================================================
// INTERNAL HELPERS
// =========================================================================
private function _delete_from_cache($cache_key) {
$this->CI->load->driver('cache', [
'adapter' => $this->CI->config->item('cache_adapter') ?? 'file',
'backup' => $this->CI->config->item('cache_backup') ?? 'file',
'key_prefix' => $this->CI->config->item('cache_key_prefix') ?? ''
]);
$this->CI->cache->delete($cache_key);
}
}

View File

@@ -0,0 +1,422 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* EqslBulkDownloader - Parallel eQSL Image Download Library
*
* Uses curl_multi to download multiple eQSL card images in parallel
* while respecting eQSL's rate limits through batch processing.
*/
class EqslBulkDownloader {
const CONCURRENCY = 10; // Number of parallel downloads
const BATCH_DELAY = 2; // Delay between batches (seconds)
const MAX_BATCH_SIZE = 150; // Safety limit per request
const TIMEOUT = 30; // Request timeout (seconds)
private $ci; // CodeIgniter instance
private $imagePath; // Path to save images
private $errors = array(); // Error tracking
private $successCount = 0; // Success counter
/**
* Constructor
*/
public function __construct() {
$this->ci =& get_instance();
$this->ci->load->library('electronicqsl');
$this->ci->load->model('Eqsl_images');
$this->ci->load->model('user_model');
$this->ci->load->model('logbook_model');
// Get image path
$this->imagePath = $this->ci->Eqsl_images->get_imagePath('p');
log_message('info', 'EqslBulkDownloader initialized with concurrency=' . self::CONCURRENCY);
}
/**
* Download multiple QSO images in parallel batches
* @param array $qsos Array of QSO records from eqsl_not_yet_downloaded()
* @return array Results with 'success_count', 'error_count', 'errors'
*/
public function downloadBatch($qsos) {
if (empty($qsos)) {
return array(
'success_count' => 0,
'error_count' => 0,
'errors' => array()
);
}
log_message('info', 'Starting parallel download of ' . count($qsos) . ' images');
// Reset counters
$this->errors = array();
$this->successCount = 0;
// Process in chunks based on concurrency
$chunks = array_chunk($qsos, self::CONCURRENCY);
$totalChunks = count($chunks);
foreach ($chunks as $chunkIndex => $qsoChunk) {
log_message('debug', 'Processing batch ' . ($chunkIndex + 1) . ' of ' . $totalChunks);
// Download this batch in parallel
$this->downloadBatchInternal($qsoChunk);
// Add delay between batches (except for last batch)
if ($chunkIndex < $totalChunks - 1) {
log_message('debug', 'Sleeping for ' . self::BATCH_DELAY . ' seconds');
sleep(self::BATCH_DELAY);
}
}
return array(
'success_count' => $this->successCount,
'error_count' => count($this->errors),
'errors' => $this->errors
);
}
/**
* Download a batch of QSOs in parallel using curl_multi
* @param array $qsos Array of QSO records
*/
private function downloadBatchInternal($qsos) {
$userId = $this->ci->session->userdata('user_id');
$query = $this->ci->user_model->get_by_id($userId);
$userRow = $query->row();
$password = $userRow->user_eqsl_password;
$this->ci->load->model('stations');
// Prepare URLs for all QSOs
$downloads = array();
foreach ($qsos as $qso) {
// Get station callsign from station_profile
$station = $this->ci->stations->profile($qso['station_id']);
if ($station && $station->num_rows() > 0) {
$qso['COL_STATION_CALLSIGN'] = $station->row()->station_callsign;
} else {
log_message('error', 'Station not found for station_id: ' . $qso['station_id']);
// Add error and skip this QSO
$this->errors[] = array(
'date' => $qso['COL_TIME_ON'],
'call' => $qso['COL_CALL'],
'mode' => $qso['COL_MODE'],
'submode' => isset($qso['COL_SUBMODE']) ? $qso['COL_SUBMODE'] : '',
'status' => 'Station profile not found',
'qsoid' => $qso['COL_PRIMARY_KEY']
);
continue;
}
$url = $this->buildImageUrl($qso, $password);
$downloads[] = array(
'url' => $url,
'qso' => $qso
);
}
// Execute parallel downloads
$results = $this->executeParallelDownloads($downloads);
// Process results
foreach ($results as $result) {
if ($result['success']) {
$this->successCount++;
} else {
$this->errors[] = array(
'date' => $result['qso']['COL_TIME_ON'],
'call' => $result['qso']['COL_CALL'],
'mode' => $result['qso']['COL_MODE'],
'submode' => isset($result['qso']['COL_SUBMODE']) ? $result['qso']['COL_SUBMODE'] : '',
'status' => $result['error'],
'qsoid' => $result['qso']['COL_PRIMARY_KEY']
);
// Break on rate limit
if ($result['error'] === 'Rate Limited') {
log_message('warning', 'eQSL rate limit detected, stopping batch');
break;
}
}
}
}
private function executeParallelDownloads($downloads) {
$results = array();
$mh = curl_multi_init();
$curlMap = array(); // Map curl handles to download info
// Step 1: Fetch all HTML pages in parallel to get image URLs
foreach ($downloads as $index => $download) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $download['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog-eQSL/1.0');
curl_multi_add_handle($mh, $ch);
$curlMap[(int)$ch] = array(
'index' => $index,
'qso' => $download['qso'],
'handle' => $ch
);
}
// Execute all HTML page handles simultaneously
$active = null;
do {
$status = curl_multi_exec($mh, $active);
if ($active) {
curl_multi_select($mh); // Wait for activity
}
} while ($active && $status == CURLM_OK);
// Collect HTML responses and extract image URLs
$imageDownloads = array();
$tempResults = array(); // Store intermediate results
foreach ($curlMap as $curlInfo) {
$ch = $curlInfo['handle'];
$qso = $curlInfo['qso'];
$qsoId = $qso['COL_PRIMARY_KEY'];
$content = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$tempResults[$qsoId] = array(
'qso' => $qso,
'success' => false,
'error' => ''
);
if ($content !== false && $httpCode == 200) {
// Parse HTML to find image URL
$imageUrl = $this->parseImageResponse($content, $qsoId);
if ($imageUrl['success']) {
// Queue for parallel image download
$imageDownloads[] = array(
'url' => $imageUrl['url'],
'qso' => $qso
);
} else {
// Parsing failed
$tempResults[$qsoId]['error'] = $imageUrl['error'];
}
} else {
// HTTP error
$tempResults[$qsoId]['error'] = 'HTTP ' . $httpCode;
}
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
// Step 2: Download all actual images in parallel
$downloadStatus = $this->downloadImagesInParallel($imageDownloads);
// Build results array from download status and temp results
foreach ($downloads as $download) {
$qso = $download['qso'];
$qsoId = $qso['COL_PRIMARY_KEY'];
if (isset($downloadStatus[$qsoId])) {
// Image was downloaded
$results[] = array(
'qso' => $qso,
'success' => $downloadStatus[$qsoId]['success'],
'error' => $downloadStatus[$qsoId]['error']
);
} elseif (isset($tempResults[$qsoId])) {
// HTML parsing failed or HTTP error
$results[] = $tempResults[$qsoId];
} else {
// Shouldn't happen
$results[] = array(
'qso' => $qso,
'success' => false,
'error' => 'Unknown error'
);
}
}
return $results;
}
private function downloadImagesInParallel($imageDownloads) {
$mh = curl_multi_init();
$curlMap = array();
$status = array();
// Initialize all curl handles for image downloads
foreach ($imageDownloads as $index => $download) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $download['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog-eQSL/1.0');
curl_multi_add_handle($mh, $ch);
$curlMap[$index] = array(
'handle' => $ch,
'qso' => $download['qso']
);
}
// Execute all image handles simultaneously
$active = null;
do {
$status_curl = curl_multi_exec($mh, $active);
if ($active) {
curl_multi_select($mh);
}
} while ($active && $status_curl == CURLM_OK);
// Save images and build status array
foreach ($curlMap as $item) {
$ch = $item['handle'];
$qso = $item['qso'];
$qsoId = $qso['COL_PRIMARY_KEY'];
$content = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$status[$qsoId] = array(
'success' => false,
'error' => ''
);
if ($content !== false && $httpCode == 200) {
// Check if already downloaded
if ($this->ci->Eqsl_images->get_image($qsoId) == "No Image") {
// Save image
$filename = uniqid() . '.jpg';
$imagePath = $this->imagePath . '/' . $filename;
if (file_put_contents($imagePath, $content) !== false) {
$this->ci->Eqsl_images->save_image($qsoId, $filename);
$status[$qsoId]['success'] = true;
log_message('debug', 'Successfully downloaded image for QSO ' . $qsoId);
} else {
log_message('error', 'Failed to save image for QSO ' . $qsoId);
$status[$qsoId]['error'] = 'Failed to save image';
}
} else {
// Already exists
$status[$qsoId]['success'] = true;
log_message('info', 'Image already exists for QSO ' . $qsoId);
}
} else {
log_message('error', 'Failed to download image for QSO ' . $qsoId . ' HTTP ' . $httpCode);
$status[$qsoId]['error'] = 'HTTP ' . $httpCode;
}
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
return $status;
}
private function buildImageUrl($qso, $password) {
$qso_timestamp = strtotime($qso['COL_TIME_ON']);
$callsign = $qso['COL_CALL'];
$band = $qso['COL_BAND'];
$mode = $qso['COL_MODE'];
$year = date('Y', $qso_timestamp);
$month = date('m', $qso_timestamp);
$day = date('d', $qso_timestamp);
$hour = date('H', $qso_timestamp);
$minute = date('i', $qso_timestamp);
$username = $qso['COL_STATION_CALLSIGN'];
return $this->ci->electronicqsl->card_image(
$username,
urlencode($password),
$callsign,
$band,
$mode,
$year,
$month,
$day,
$hour,
$minute
);
}
private function parseImageResponse($html, $qsoId) {
$result = array('success' => false, 'url' => '', 'error' => '');
// Check for error in HTML
if (strpos($html, 'Error') !== false) {
$result['error'] = rtrim(preg_replace('/^\s*Error: /', '', $html));
return $result;
}
// Parse HTML to find image
$dom = new domDocument;
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$dom->preserveWhiteSpace = false;
$images = $dom->getElementsByTagName('img');
if (!isset($images) || $images->length == 0) {
$h3 = $dom->getElementsByTagName('h3');
if (isset($h3) && $h3->item(0) !== null) {
$result['error'] = $h3->item(0)->nodeValue;
} else {
$result['error'] = 'Rate Limited';
}
return $result;
}
// Get first image URL
$imageSrc = "https://www.eqsl.cc" . $images->item(0)->getAttribute('src');
$result['success'] = true;
$result['url'] = $imageSrc;
return $result;
}
private function downloadAndSaveImage($url, $qsoId) {
// Check if already downloaded
if ($this->ci->Eqsl_images->get_image($qsoId) != "No Image") {
return true; // Already exists
}
// Download image
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog-eQSL/1.0');
$content = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($content === false || $httpCode != 200) {
log_message('error', 'Failed to download image from: ' . $url);
return false;
}
// Save image
$filename = uniqid() . '.jpg';
$imagePath = $this->imagePath . '/' . $filename;
if (file_put_contents($imagePath, $content) !== false) {
$this->ci->Eqsl_images->save_image($qsoId, $filename);
return true;
} else {
log_message('error', 'Failed to save image to: ' . $imagePath);
return false;
}
}
}

View File

@@ -85,53 +85,48 @@ class EqslImporter
// Let's use cURL instead of file_get_contents
// begin script
$ch = curl_init();
try {
// basic curl options for all requests
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// basic curl options for all requests
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// use the URL and params we built
curl_setopt($ch, CURLOPT_URL, $eqsl_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $eqsl_params);
// use the URL and params we built
curl_setopt($ch, CURLOPT_URL, $eqsl_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $eqsl_params);
$input = curl_exec($ch);
$chi = curl_getinfo($ch);
$input = curl_exec($ch);
$chi = curl_getinfo($ch);
// "You have no log entries" -> Nothing else to do here
// "Your ADIF log file has been built" -> We've got an ADIF file we need to grab.
// "You have no log entries" -> Nothing else to do here
// "Your ADIF log file has been built" -> We've got an ADIF file we need to grab.
if ($chi['http_code'] == "200") {
if (stristr($input, "You have no log entries")) {
return $this->result('There are no QSLs waiting for download at eQSL.cc.'); // success
} else if (stristr($input, "Error: No such Username/Password found")) {
return $this->result('No such Username/Password found This could mean the wrong callsign or the wrong password, or the user does not exist.'); // warning
} else {
if (stristr($input, "Your ADIF log file has been built")) {
// Get all the links on the page and grab the URL for the ADI file.
$regexp = "<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>";
if (preg_match_all("/$regexp/siU", $input, $matches)) {
foreach ($matches[2] as $match) {
// Look for the link that has the .adi file, and download it to $file
if (substr($match, -4, 4) == ".adi") {
file_put_contents($this->adif_file, file_get_contents("https://eqsl.cc/qslcard/" . $match));
return $this->import();
}
if ($chi['http_code'] == "200") {
if (stristr($input, "You have no log entries")) {
return $this->result('There are no QSLs waiting for download at eQSL.cc.'); // success
} else if (stristr($input, "Error: No such Username/Password found")) {
return $this->result('No such Username/Password found This could mean the wrong callsign or the wrong password, or the user does not exist.'); // warning
} else {
if (stristr($input, "Your ADIF log file has been built")) {
// Get all the links on the page and grab the URL for the ADI file.
$regexp = "<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>";
if (preg_match_all("/$regexp/siU", $input, $matches)) {
foreach ($matches[2] as $match) {
// Look for the link that has the .adi file, and download it to $file
if (substr($match, -4, 4) == ".adi") {
file_put_contents($this->adif_file, file_get_contents("https://eqsl.cc/qslcard/" . $match));
return $this->import();
}
}
}
}
} else {
if ($chi['http_code'] == "500") {
return $this->result('eQSL.cc is experiencing issues. Please try importing QSOs later.'); // warning
}
}
return $this->result('It seems that the eQSL site has changed. Please open up an issue on GitHub.');
} finally {
// Close cURL handle
curl_close($ch);
} else {
if ($chi['http_code'] == "500") {
return $this->result('eQSL.cc is experiencing issues. Please try importing QSOs later.'); // warning
}
}
return $this->result('It seems that the eQSL site has changed. Please open up an issue on GitHub.');
}
// Read the ADIF file and set QSO confirmation status according to the settings

View File

@@ -171,7 +171,7 @@ class Frequency {
$Band = "160m";
} else if ($Frequency > 3000000 && $Frequency < 4000000) {
$Band = "80m";
} else if ($Frequency > 5350000 && $Frequency < 5367000) {
} else if ($Frequency >= 5330000 && $Frequency < 5405000) {
$Band = "60m";
} else if ($Frequency > 6000000 && $Frequency < 8000000) {
$Band = "40m";
@@ -195,7 +195,7 @@ class Frequency {
$Band = "2m";
} else if ($Frequency > 218000000 && $Frequency < 226000000) {
$Band = "1.25m";
} else if ($Frequency > 420000000 && $Frequency < 450000000) {
} else if ($Frequency >= 420000000 && $Frequency < 450000000) {
$Band = "70cm";
} else if ($Frequency > 900000000 && $Frequency < 930000000) {
$Band = "33cm";

View File

@@ -0,0 +1,172 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
/**
* Garbage collection library for file cache
*
* Since we need a garbage collector for file caching, we implement a dynamic
* probability check based on traffic patterns to avoid performance hits on
* high-traffic sites while still ensuring regular cleanup on low-traffic sites.
*
* Why we don't use $this->cache->clean()? Because this deletes everything. We only want to delete expired files.
*
* 2026, Fabian Berg, HB9HIL
*/
class GarbageCollector {
private $CI;
public function __construct()
{
$this->CI =& get_instance();
}
/**
* Run garbage collection for file cache with traffic-based probability and interval checks.
*
* @return int Number of deleted files, or 0 on failure
*/
public function run()
{
// The gc checkfile path has to exist on every possible environment. So we choose sys_get_temp_dir()
// and hash it with FCPATH to avoid collisions between different Wavelog installations on the same server.
$gc_file = sys_get_temp_dir() . '/ci_gc_last_run_' . md5(FCPATH) . '.txt';
// The garbage collection should run around every 4 hours
$gc_interval = 3600 * 4;
// Traffic metric: Requests since last GC
$data = file_exists($gc_file) ? (json_decode(file_get_contents($gc_file), true) ?: []) : [];
$last_run = $data['time'] ?? 0;
$request_count = ($data['count'] ?? 0) + 1; // This is also a request so +1
// Dynamic probability based on traffic to reduce load on high-traffic installations
// This logic is inverted to a normal human brain. Higher traffic = lower probability to go through the next check.
if ($request_count < 100) {
$probability = 100; // Low-Traffic: check on every request (100% pass the probability check)
} elseif ($request_count < 1000) {
$probability = 50; // Medium-Traffic: every 2nd request (50% pass the probability check)
} else {
$probability = 10; // High-Traffic: every 10th request (only 10% pass the probability check)
}
// We do the probability check first. Let's play some lottery
if (rand(1, 100) > $probability) {
// Oh snag, we did not hit the probability but still need to update the request count
// The +1 was already added above
$this->_update_gc_file($gc_file, $last_run, $request_count);
return 0;
}
// Oh dear, we hit the probability. Now check if enough time has passed since last run.
if (time() - $last_run < $gc_interval) {
// Nope, so just update the request count
$this->_update_gc_file($gc_file, $last_run, $request_count);
return 0;
}
// Alright, let's do some garbage collection!
// We use a lock file to prevent multiple simultaneous GC runs
// in case of high traffic. Only one process should do the GC at a time.
$lock_file = $gc_file . '.lock';
// Try to acquire the lock
$fp = fopen($lock_file, 'c');
if ($fp === FALSE) {
return 0;
}
// If we cannot acquire the lock, another process is already doing GC
// So we just return and do nothing so the other process can finish
if ( ! flock($fp, LOCK_EX | LOCK_NB)) {
fclose($fp);
return 0;
}
log_message('info', 'Starting file cache garbage collection...');
try {
// Perform garbage collection itself (without loading the cache driver)
$result = $this->_run_garbage_collector();
// Update the GC file with the current time and reset request count to 0
$this->_update_gc_file($gc_file, time(), 0);
} finally {
// Release the lock and close the file
// This will happen even if an exception occurs during GC so we don't deadlock
flock($fp, LOCK_UN);
fclose($fp);
@unlink($lock_file);
}
log_message('info', 'File cache garbage collection completed. Deleted ' . $result . ' expired files.');
return $result;
}
/**
* Run file cache garbage collection without loading the cache driver.
*
* @return int Number of deleted files, or 0 on failure
*/
public function _run_garbage_collector()
{
$cache_path = $this->CI->config->item('cache_path') == '' ? APPPATH.'cache/' : $this->CI->config->item('cache_path');
log_message('debug', 'GarbageCollector: Scanning cache path ' . $cache_path);
if ( ! is_dir($cache_path))
{
log_message('error', 'GarbageCollector: Cache path is not a directory or does not exist: ' . $cache_path);
return 0;
}
// We need to ignore some CI specific files
$ignore_files = [
'index.html',
'.htaccess'
];
$deleted = 0;
$current_time = time();
if ($handle = opendir($cache_path))
{
while (($file = readdir($handle)) !== FALSE)
{
if ($file === '.' || $file === '..' || in_array($file, $ignore_files))
{
continue;
}
$filepath = $cache_path.$file;
if (is_file($filepath))
{
$data = @unserialize(file_get_contents($filepath));
if (is_array($data) && isset($data['time'], $data['ttl']))
{
// Check if TTL is set and file has expired
if ($data['ttl'] > 0 && $current_time > $data['time'] + $data['ttl'])
{
if (unlink($filepath))
{
$deleted++;
}
}
}
}
}
closedir($handle);
}
return $deleted;
}
private function _update_gc_file($gc_file, $time, $count) {
file_put_contents($gc_file, json_encode(['time' => $time, 'count' => $count]));
}
}

View File

@@ -12,6 +12,7 @@ class Genfunctions
(($postdata['qrz'] ?? '') != '') ||
(($postdata['lotw'] ?? '') != '') ||
(($postdata['qsl'] ?? '') != '') ||
(($postdata['dcl'] ?? '') != '') ||
(($postdata['eqsl'] ?? '') != '') ) {
$sql .= ' and (';
if (($postdata['qsl'] ?? '') != '') {
@@ -29,6 +30,9 @@ class Genfunctions
if (($postdata['clublog'] ?? '') != '') {
array_push($qsl, "COL_CLUBLOG_QSO_DOWNLOAD_STATUS = 'Y'");
}
if (($postdata['dcl'] ?? '') != '') {
array_push($qsl, "COL_DCL_QSL_RCVD = 'Y'");
}
if (count($qsl) > 0) {
$sql .= implode(' or ', $qsl);
} else {
@@ -68,6 +72,9 @@ class Genfunctions
if (($postdata['eqsl'] ?? '')!= '' ) {
$qsl .= "E";
}
if (($postdata['dcl'] ?? '')!= '' ) {
$qsl .= "D";
}
if (($postdata['clublog'] ?? '')!= '' ) {
$qsl .= "C";
}

View File

@@ -0,0 +1,305 @@
<?php if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
* Geojson Library
*
* This library provides GeoJSON-based geographic operations for Wavelog,
* used for determining states, provinces, and other administrative subdivisions
* from gridsquare locators using point-in-polygon detection.
*
* Main functionality:
* - Convert Maidenhead gridsquares to lat/lng coordinates
* - Determine state/province from coordinates using GeoJSON boundary data
* - Point-in-polygon detection for Polygon and MultiPolygon geometries
*
*/
class Geojson {
/**
* DXCC entities that support state/province subdivision lookups
*
* Key: DXCC number
* Value: Array with 'name' and 'enabled' flag
*/
const SUPPORTED_STATES = [
1 => ['name' => 'Canada', 'enabled' => true], // 13 provinces/territories
6 => ['name' => 'Alaska', 'enabled' => true], // 1 state
27 => ['name' => 'Belarus', 'enabled' => true], // 7 subdivisions
29 => ['name' => 'Canary Islands', 'enabled' => true], // 2 provinces
32 => ['name' => 'Ceuta & Melilla', 'enabled' => true], // 2 autonomous cities
50 => ['name' => 'Mexico', 'enabled' => true], // 32 states
100 => ['name' => 'Argentina', 'enabled' => true], // 24 subdivisions
108 => ['name' => 'Brazil', 'enabled' => true], // 27 subdivisions
110 => ['name' => 'Hawaii', 'enabled' => true], // 1 state
112 => ['name' => 'Chile', 'enabled' => true], // 16 regions
137 => ['name' => 'Republic of Korea', 'enabled' => true], // 17 subdivisions
144 => ['name' => 'Uruguay', 'enabled' => true], // 19 subdivisions
148 => ['name' => 'Venezuela', 'enabled' => true], // 24 states
149 => ['name' => 'Azores', 'enabled' => true], // 1 autonomous region
150 => ['name' => 'Australia', 'enabled' => true], // 8 subdivisions
163 => ['name' => 'Papua New Guinea', 'enabled' => true], // 22 provinces
170 => ['name' => 'New Zealand', 'enabled' => true], // 16 regions
209 => ['name' => 'Belgium', 'enabled' => true], // 11 subdivisions
212 => ['name' => 'Bulgaria', 'enabled' => true], // 28 subdivisions
214 => ['name' => 'Corsica', 'enabled' => true], // 2 departments (2A, 2B)
225 => ['name' => 'Sardinia', 'enabled' => true], // 5 provinces
227 => ['name' => 'France', 'enabled' => true], // 96 departments
230 => ['name' => 'Germany', 'enabled' => true], // 16 federal states
239 => ['name' => 'Hungary', 'enabled' => true], // 20 subdivisions
245 => ['name' => 'Ireland', 'enabled' => true], // 27 subdivisions
248 => ['name' => 'Italy', 'enabled' => true], // 107 provinces
256 => ['name' => 'Madeira Islands', 'enabled' => true], // 1 autonomous region
263 => ['name' => 'Netherlands', 'enabled' => true], // 12 provinces
266 => ['name' => 'Norway', 'enabled' => true], // 15 counties
269 => ['name' => 'Poland', 'enabled' => true], // 16 voivodeships
272 => ['name' => 'Portugal', 'enabled' => true], // 18 districts
275 => ['name' => 'Romania', 'enabled' => true], // 42 counties
281 => ['name' => 'Spain', 'enabled' => true], // 47 provinces
284 => ['name' => 'Sweden', 'enabled' => true], // 21 subdivisions
287 => ['name' => 'Switzerland', 'enabled' => true], // 26 cantons
291 => ['name' => 'USA', 'enabled' => true], // 52 states/territories
318 => ['name' => 'China', 'enabled' => true], // 31 provinces
324 => ['name' => 'India', 'enabled' => true], // 36 states/territories
339 => ['name' => 'Japan', 'enabled' => true], // 47 prefectures
386 => ['name' => 'Taiwan', 'enabled' => true], // 22 subdivisions
497 => ['name' => 'Croatia', 'enabled' => true], // 21 subdivisions
];
private $qra;
private $geojsonFile = null;
private $geojsonData = null;
public function __construct($dxcc = null) {
$CI =& get_instance();
$CI->load->library('qra');
$this->qra = $CI->qra;
if ($dxcc !== null) {
$this->geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
$this->geojsonData = $this->loadGeoJsonFile($geojsonFile);
}
}
// ============================================================================
// PUBLIC API METHODS - Main entry points for state lookup
// ============================================================================
/**
* Find state from grid square locator
*
* This is the main method used by the application to determine state/province
* from a Maidenhead gridsquare.
*
* @param string $gridsquare Maidenhead grid square (e.g., "FM18lw")
* @param int $dxcc DXCC entity number (e.g., 291 for USA)
* @return array|null State properties (including 'code' and 'name') or null if not found
*/
public function findStateFromGridsquare($gridsquare, $dxcc) {
$coords = $this->gridsquareToLatLng($gridsquare);
if ($coords === null) {
return null;
}
return $this->findStateByDxcc($coords['lat'], $coords['lng'], $dxcc);
}
/**
* Find state by DXCC entity number and coordinates
*
* This method loads the appropriate GeoJSON file for the DXCC entity
* and searches for the state/province containing the given coordinates.
*
* @param float $lat Latitude
* @param float $lng Longitude
* @param int $dxcc DXCC entity number (e.g., 291 for USA)
* @return array|null State properties or null if not found
*/
public function findStateByDxcc($lat, $lng, $dxcc) {
// Check if state lookup is supported for this DXCC
if (!$this->isStateSupported($dxcc)) {
return null;
}
if ($this->geojsonFile === null) {
$this->geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
$this->geojsonData = $this->loadGeoJsonFile($this->geojsonFile);
}
if ($this->geojsonData === null) {
return null;
}
return $this->findFeatureContainingPoint($lat, $lng, $this->geojsonData);
}
/**
* Check if state lookup is supported for given DXCC entity
*
* @param int $dxcc DXCC entity number
* @return bool True if state lookup is supported and enabled
*/
public function isStateSupported($dxcc) {
return isset(self::SUPPORTED_STATES[$dxcc]) && self::SUPPORTED_STATES[$dxcc]['enabled'] === true;
}
/**
* Retrieve list of DXCC entities that support state/province lookups
*
* @return array List of supported DXCC entities
*/
public function getSupportedDxccs() {
return self::SUPPORTED_STATES;
}
// ============================================================================
// COORDINATE CONVERSION
// ============================================================================
/**
* Convert Maidenhead grid square to latitude/longitude
*
* Uses the Qra library for gridsquare conversion.
* Supports 2, 4, 6, 8, and 10 character gridsquares.
* Also supports grid lines and grid corners (comma-separated).
*
* @param string $gridsquare Maidenhead grid square (e.g., "JO70va")
* @return array|null Array with 'lat' and 'lng' or null on error
*/
public function gridsquareToLatLng($gridsquare) {
if (!is_string($gridsquare) || strlen($gridsquare) < 2) {
return null;
}
$result = $this->qra->qra2latlong($gridsquare);
if ($result === false || !is_array($result) || count($result) < 2) {
return null;
}
// Qra library returns [lat, lng], we need to return associative array
return [
'lat' => $result[0],
'lng' => $result[1]
];
}
// ============================================================================
// GEOJSON FILE OPERATIONS
// ============================================================================
/**
* Load and parse a GeoJSON file
*
* @param string $filepath Path to GeoJSON file (relative to FCPATH)
* @return array|null Decoded GeoJSON data or null on error
*/
public function loadGeoJsonFile($filepath) {
$fullpath = FCPATH . $filepath;
if (!file_exists($fullpath)) {
return null;
}
$geojsonData = file_get_contents($fullpath);
if ($geojsonData === false) {
return null;
}
// Remove BOM if present (UTF-8, UTF-16, UTF-32)
$geojsonData = preg_replace('/^\xEF\xBB\xBF|\xFF\xFE|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00/', '', $geojsonData);
// Additional cleanup: trim whitespace
$geojsonData = trim($geojsonData);
$data = json_decode($geojsonData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
return $data;
}
// ============================================================================
// GEOMETRIC ALGORITHMS - Point-in-polygon detection
// ============================================================================
/**
* Check if a point (latitude, longitude) is inside a polygon
* Uses ray casting algorithm
*
* @param float $lat Latitude of the point
* @param float $lng Longitude of the point
* @param array $polygon GeoJSON polygon coordinates array [[[lng, lat], [lng, lat], ...]]
* @return bool True if point is inside polygon, false otherwise
*/
public function isPointInPolygon($lat, $lng, $polygon) {
if (!is_numeric($lat) || !is_numeric($lng) || !is_array($polygon) || empty($polygon)) {
return false;
}
$inside = false;
$count = count($polygon);
// Ray casting algorithm
for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) {
$xi = $polygon[$i][0]; // longitude
$yi = $polygon[$i][1]; // latitude
$xj = $polygon[$j][0]; // longitude
$yj = $polygon[$j][1]; // latitude
$intersect = (($yi > $lat) !== ($yj > $lat))
&& ($lng < ($xj - $xi) * ($lat - $yi) / ($yj - $yi) + $xi);
if ($intersect) {
$inside = !$inside;
}
}
return $inside;
}
/**
* Find which feature in a GeoJSON FeatureCollection contains a given point
*
* @param float $lat Latitude of the point
* @param float $lng Longitude of the point
* @param array $geojsonData Decoded GeoJSON FeatureCollection
* @return array|null Feature properties if found, null otherwise
*/
public function findFeatureContainingPoint($lat, $lng, $geojsonData) {
if (!isset($geojsonData['features']) || !is_array($geojsonData['features'])) {
return null;
}
foreach ($geojsonData['features'] as $feature) {
if (!isset($feature['geometry']['coordinates']) || !isset($feature['geometry']['type'])) {
continue;
}
$geometryType = $feature['geometry']['type'];
$coordinates = $feature['geometry']['coordinates'];
// Handle Polygon geometry
if ($geometryType === 'Polygon') {
// For Polygon, coordinates[0] is the outer ring
if ($this->isPointInPolygon($lat, $lng, $coordinates[0])) {
return $feature['properties'];
}
}
// Handle MultiPolygon geometry
elseif ($geometryType === 'MultiPolygon') {
foreach ($coordinates as $polygon) {
// For MultiPolygon, each polygon is [[[lng,lat],...]]
// We need to pass just the outer ring (first element)
if ($this->isPointInPolygon($lat, $lng, $polygon[0])) {
return $feature['properties'];
}
}
}
}
return null;
}
}

View File

@@ -7,6 +7,8 @@
class Hamqth {
public $callbookname = 'HamQTH';
// Return session key
public function session($username, $password) {
// URL to the XML Source
@@ -20,7 +22,10 @@ class Hamqth {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$xml = curl_exec($ch);
curl_close($ch);
if(curl_errno($ch)) {
log_message('error', 'Hamqth query failed: '.curl_strerror(curl_errno($ch))." (".curl_errno($ch).")");
return false;
}
// Create XML object
$xml = simplexml_load_string($xml);
@@ -45,7 +50,6 @@ class Hamqth {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -72,7 +76,6 @@ class Hamqth {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -121,8 +124,12 @@ class Hamqth {
$data['us_county'] = '';
}
} finally {
return $data;
}
} finally {
return $data;
}
}
public function sourcename() {
return $this->callbookname;
}
}

View File

@@ -0,0 +1,67 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
require_once ('./src/phpMQTT.php');
class Mh {
private $ci;
private $mqsettings;
private $mqtt;
public function __construct() {
$this->ci = & get_instance();
$this->mqsettings['server']=($this->ci->config->item('mqtt_server') ?? '');
$this->mqsettings['port']=($this->ci->config->item('mqtt_port') ?? 1883);
$this->mqsettings['user']=($this->ci->config->item('mqtt_username') ?? null);
$this->mqsettings['pass']=($this->ci->config->item('mqtt_password') ?? null);
$this->mqsettings['prefix']=($this->ci->config->item('mqtt_prefix') ?? 'wavelog/');
}
public function connect() {
$server = $this->mqsettings['server'];
$port = $this->mqsettings['port'];
$clientId = uniqid('wl_');
if ($this->mqsettings['server'] != '') {
try {
$this->mqtt = @new Wavelog\phpMQTT($server, $port, $clientId);
if (!@$this->mqtt->connect(true, NULL, $this->mqsettings['user'],$this->mqsettings['pass'])) {
throw new Exception('Failed to connect to MQTT broker');
}
register_shutdown_function([$this, 'disconnect']);
return true;
} catch (Exception $e) {
log_message('error','Error while trying to connect to MQTT: '.$e->getMessage());
$this->mqtt=false;
}
}
}
public function disconnect() {
if ($this->mqtt) {
log_message('debug', 'disconnect from MQTT broker');
$this->mqtt->close();
}
}
public function wl_event($topic, $message) {
if ($this->mqsettings['server'] != '') {
if (!($this->mqtt)) {
$this->connect();
}
if ($this->mqtt) { // Failsafe. Check if REALLY connected before trying to puv
log_message('debug', 'published '.$this->mqsettings['prefix'].$topic.' -> '.$message.' to MQTT broker');
$this->publish($this->mqsettings['prefix'].$topic, $message);
}
}
}
private function publish($topic, $message) {
if ($this->mqtt) {
$this->mqtt->publish($topic, $message, 0);
} else {
log_message('error', 'Cannot publish message: MQTT connection not established');
}
}
}

View File

@@ -58,9 +58,6 @@ class Pota
$summit_info = curl_exec($ch);
// Close cURL handle
curl_close($ch);
return $summit_info;
}
}

View File

@@ -199,6 +199,28 @@ class Qra {
return [atan2(($z / $n), sqrt($x * $x + $y * $y)) * 180 / pi(), atan2($y, $x) * 180 / pi()];
}
function validate_grid($grid) {
// (Try to) Detect placeholders like AA00aa (returned from qrz.com for example)
if (strlen($grid) == 6 && strtoupper($grid) == 'AA00AA') {
return false;
}
// Allow 6-digit locator
if (preg_match('/^[A-Ra-r]{2}[0-9]{2}[A-Za-z]{2}$/', $grid)) return true;
// Allow 4-digit locator
else if (preg_match('/^[A-Ra-r]{2}[0-9]{2}$/', $grid)) return true;
// Allow 4-digit grid line
else if (preg_match('/^[A-Ra-r]{2}[0-9]{2},[A-Ra-r]{2}[0-9]{2}$/', $grid)) return true;
// Allow 4-digit grid corner
else if (preg_match('/^[A-Ra-r]{2}[0-9]{2},[A-Ra-r]{2}[0-9]{2},[A-Ra-r]{2}[0-9]{2},[A-Ra-r]{2}[0-9]{2}$/', $grid)) return true;
// Allow 2-digit locator
else if (preg_match('/^[A-Ra-r]{2}$/', $grid)) return true;
// Allow 8-digit locator
else if (preg_match('/^[A-Ra-r]{2}[0-9]{2}[A-Za-z]{2}[0-9]{2}$/', $grid)) return true;
// Allow 10-digit locator
else if (preg_match('/^[A-Ra-r]{2}[0-9]{2}[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}$/', $grid)) return true;
return false;
}
}

View File

@@ -1,18 +1,19 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/*
Controls the interaction with the QRZ.com Subscription based XML API.
*/
class Qrz {
public $callbookname = 'QRZ';
// Return session key
public function session($username, $password) {
// URL to the XML Source
$ci = & get_instance();
$xml_feed_url = 'https://xmldata.qrz.com/xml/current/?username='.$username.';password='.urlencode($password).';agent=wavelog';
// CURL Functions
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $xml_feed_url);
@@ -22,23 +23,22 @@ class Qrz {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
// Return Session Key
return (string) $xml->Session->Key;
}
// Set Session Key session.
public function set_session($username, $password) {
$ci = & get_instance();
// URL to the XML Source
$xml_feed_url = 'https://xmldata.qrz.com/xml/current/?username='.$username.';password='.urlencode($password).';agent=wavelog';
// CURL Functions
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $xml_feed_url);
@@ -48,19 +48,17 @@ class Qrz {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
$key = (string) $xml->Session->Key;
$ci->session->set_userdata('qrz_session_key', $key);
return true;
}
public function search($callsign, $key, $use_fullname = false, $reduced = false) {
$data = null;
$ci = & get_instance();
@@ -78,72 +76,118 @@ class Qrz {
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpcode != 200) return $data['error'] = 'Problems with qrz.com communication'; // Exit function if no 200. If request fails, 0 is returned
if ($httpcode != 200) {
$message = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
log_message('debug', 'QRZ.com search for callsign: ' . $callsign . ' returned message: ' . $message . ' HTTP code: ' . $httpcode);
return $data['error'] = 'Problems with qrz.com communication'; // Exit function if no 200. If request fails, 0 is returned
}
// Create XML object
$xml = simplexml_load_string($xml);
if (!empty($xml->Session->Error)) {
return $data['error'] = $xml->Session->Error;
}
// Return Required Fields
// Map all QRZ XML fields according to API specification
$data['callsign'] = (string)$xml->Callsign->call;
if ($use_fullname === true) {
$data['name'] = (string)$xml->Callsign->fname. ' ' . (string)$xml->Callsign->name;
} else {
$data['name'] = (string)$xml->Callsign->fname;
}
$data['xref'] = (string)$xml->Callsign->xref;
$data['aliases'] = (string)$xml->Callsign->aliases;
$data['dxcc'] = (string)$xml->Callsign->dxcc;
$data['fname'] = (string)$xml->Callsign->fname;
$data['name_last'] = (string)$xml->Callsign->name;
$data['addr1'] = (string)$xml->Callsign->addr1;
$data['addr2'] = (string)$xml->Callsign->addr2;
$data['state'] = (string)$xml->Callsign->state;
$data['zip'] = (string)$xml->Callsign->zip;
$data['country'] = (string)$xml->Callsign->country;
$data['ccode'] = (string)$xml->Callsign->ccode;
$data['lat'] = (string)$xml->Callsign->lat;
$data['lon'] = (string)$xml->Callsign->lon;
$data['grid'] = (string)$xml->Callsign->grid;
$data['county'] = (string)$xml->Callsign->county;
$data['fips'] = (string)$xml->Callsign->fips;
$data['land'] = (string)$xml->Callsign->land;
$data['efdate'] = (string)$xml->Callsign->efdate;
$data['expdate'] = (string)$xml->Callsign->expdate;
$data['p_call'] = (string)$xml->Callsign->p_call;
$data['class'] = (string)$xml->Callsign->class;
$data['codes'] = (string)$xml->Callsign->codes;
$data['qslmgr'] = (string)$xml->Callsign->qslmgr;
$data['email'] = (string)$xml->Callsign->email;
$data['url'] = (string)$xml->Callsign->url;
$data['u_views'] = (string)$xml->Callsign->u_views;
$data['bio'] = (string)$xml->Callsign->bio;
$data['biodate'] = (string)$xml->Callsign->biodate;
$data['image'] = (string)$xml->Callsign->image;
$data['imageinfo'] = (string)$xml->Callsign->imageinfo;
$data['serial'] = (string)$xml->Callsign->serial;
$data['moddate'] = (string)$xml->Callsign->moddate;
$data['MSA'] = (string)$xml->Callsign->MSA;
$data['AreaCode'] = (string)$xml->Callsign->AreaCode;
$data['TimeZone'] = (string)$xml->Callsign->TimeZone;
$data['GMTOffset'] = (string)$xml->Callsign->GMTOffset;
$data['DST'] = (string)$xml->Callsign->DST;
$data['eqsl'] = (string)$xml->Callsign->eqsl;
$data['mqsl'] = (string)$xml->Callsign->mqsl;
$data['cqzone'] = (string)$xml->Callsign->cqzone;
$data['ituzone'] = (string)$xml->Callsign->ituzone;
$data['born'] = (string)$xml->Callsign->born;
$data['user'] = (string)$xml->Callsign->user;
$data['lotw'] = (string)$xml->Callsign->lotw;
$data['iota'] = (string)$xml->Callsign->iota;
$data['geoloc'] = (string)$xml->Callsign->geoloc;
$data['attn'] = (string)$xml->Callsign->attn;
$data['nickname'] = (string)$xml->Callsign->nickname;
$data['name_fmt'] = (string)$xml->Callsign->name_fmt;
// Build legacy 'name' field for backward compatibility
if ($use_fullname === true) {
$data['name'] = $data['fname']. ' ' . $data['name_last'];
} else {
$data['name'] = $data['fname'];
}
// we always give back the name, no matter if reduced data or not
$data['name'] = trim($data['name']);
// Sanitize gridsquare to allow only up to 8 characters
$unclean_gridsquare = (string)$xml->Callsign->grid; // Get the gridsquare from QRZ convert to string
$clean_gridsquare = strlen($unclean_gridsquare) > 8 ? substr($unclean_gridsquare,0,8) : $unclean_gridsquare; // Trim gridsquare to 8 characters max
$unclean_gridsquare = $data['grid'];
$clean_gridsquare = strlen($unclean_gridsquare) > 8 ? substr($unclean_gridsquare,0,8) : $unclean_gridsquare;
if ($reduced == false) {
$data['gridsquare'] = $clean_gridsquare;
$data['city'] = (string)$xml->Callsign->addr2;
$data['lat'] = (string)$xml->Callsign->lat;
$data['long'] = (string)$xml->Callsign->lon;
$data['dxcc'] = (string)$xml->Callsign->dxcc;
$data['state'] = (string)$xml->Callsign->state;
$data['iota'] = (string)$xml->Callsign->iota;
$data['qslmgr'] = (string)$xml->Callsign->qslmgr;
$data['image'] = (string)$xml->Callsign->image;
$data['ituz'] = (string)$xml->Callsign->ituzone;
$data['cqz'] = (string)$xml->Callsign->cqzone;
if ($xml->Callsign->country == "United States") {
$data['us_county'] = (string)$xml->Callsign->county;
} else {
$data['us_county'] = null;
}
// Map fields for backward compatibility with existing code
$data['gridsquare'] = $clean_gridsquare;
$data['city'] = $data['addr2'];
$data['long'] = $data['lon'];
$data['ituz'] = $data['ituzone'];
$data['cqz'] = $data['cqzone'];
if ($data['country'] == "United States") {
$data['us_county'] = $data['county'];
} else {
$data['us_county'] = null;
}
if ($reduced == true) {
// Clear location-specific fields for reduced mode
$data['gridsquare'] = '';
$data['city'] = '';
$data['lat'] = '';
$data['long'] = '';
$data['dxcc'] = '';
$data['state'] = '';
$data['iota'] = '';
$data['qslmgr'] = (string)$xml->Callsign->qslmgr;
$data['image'] = (string)$xml->Callsign->image;
$data['city'] = '';
$data['lat'] = '';
$data['long'] = '';
$data['lon'] = '';
$data['dxcc'] = '';
$data['state'] = '';
$data['iota'] = '';
$data['us_county'] = '';
$data['ituz'] = '';
$data['cqz'] = '';
$data['ituzone'] = '';
$data['cqzone'] = '';
}
} finally {
return $data;
}
}
public function sourcename() {
return $this->callbookname;
}
}

View File

@@ -7,6 +7,8 @@
class Qrzcq {
public $callbookname = 'QRZCQ';
// Return session key
public function session($username, $password) {
// URL to the XML Source
@@ -22,7 +24,6 @@ class Qrzcq {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -55,7 +56,6 @@ class Qrzcq {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -85,7 +85,6 @@ class Qrzcq {
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpcode != 200) return $data['error'] = 'Problems with qrzcq.com communication'; // Exit function if no 200. If request fails, 0 is returned
// Create XML object
@@ -144,8 +143,11 @@ class Qrzcq {
}
} finally {
return $data;
}
}
public function sourcename() {
return $this->callbookname;
}
}

View File

@@ -7,6 +7,8 @@
class Qrzru {
public $callbookname = 'QRZ.ru';
// Return session key
public function session($username, $password) {
// URL to the XML Source
@@ -22,7 +24,6 @@ class Qrzru {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -48,7 +49,6 @@ class Qrzru {
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -78,7 +78,6 @@ class Qrzru {
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog/'.$ci->optionslib->get_option('version'));
$xml = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Create XML object
$xml = simplexml_load_string($xml);
@@ -118,4 +117,8 @@ class Qrzru {
return $data;
}
}
public function sourcename() {
return $this->callbookname;
}
}

View File

@@ -0,0 +1,182 @@
<?php
if (!defined('BASEPATH')) exit('No direct script access allowed');
/**
* Rate Limiting Library
*
* Implements sliding window rate limiting for API endpoints
*/
class Rate_limit {
protected $CI;
protected $cache;
protected $rate_limits;
public function __construct() {
$this->CI =& get_instance();
$this->CI->load->driver('cache', [
'adapter' => $this->CI->config->item('cache_adapter') ?? 'file',
'backup' => $this->CI->config->item('cache_backup') ?? 'file',
'key_prefix' => $this->CI->config->item('cache_key_prefix') ?? ''
]);
// Load rate limit config - if not set or empty, rate limiting is disabled
$this->rate_limits = $this->CI->config->item('api_rate_limits');
}
/**
* Check if rate limiting is enabled
*
* @return bool True if rate limiting is enabled
*/
public function is_enabled() {
return !empty($this->rate_limits) && is_array($this->rate_limits);
}
/**
* Check and enforce rate limit for an endpoint
*
* @param string $endpoint The API endpoint name
* @param string $identifier Unique identifier (API key, user ID, or IP)
* @return array Array with 'allowed' (bool) and 'retry_after' (int|null)
*/
public function check($endpoint, $identifier = null) {
// If rate limiting is disabled, always allow
if (!$this->is_enabled()) {
return ['allowed' => true, 'retry_after' => null];
}
// Get the limit for this endpoint
$limit = $this->get_limit($endpoint);
// If no limit configured for this endpoint, allow
if ($limit === null) {
return ['allowed' => true, 'retry_after' => null];
}
// Generate identifier if not provided
if ($identifier === null) {
$identifier = $this->generate_identifier();
}
$max_requests = $limit['requests'];
$window_seconds = $limit['window'];
return $this->sliding_window_check($endpoint, $identifier, $max_requests, $window_seconds);
}
/**
* Sliding window rate limit check
*
* @param string $endpoint The API endpoint name
* @param string $identifier Unique identifier for the requester
* @param int $max_requests Maximum requests allowed
* @param int $window_seconds Time window in seconds
* @return array Array with 'allowed' (bool) and 'retry_after' (int|null)
*/
protected function sliding_window_check($endpoint, $identifier, $max_requests, $window_seconds) {
$cache_key = 'rate_limit_' . md5($endpoint . '_' . $identifier);
$now = time();
// Get existing request timestamps from cache
$request_timestamps = $this->CI->cache->get($cache_key);
if ($request_timestamps === false) {
$request_timestamps = [];
}
// Filter out timestamps that are outside the time window
$window_start = $now - $window_seconds;
$request_timestamps = array_filter($request_timestamps, function($timestamp) use ($window_start) {
return $timestamp > $window_start;
});
// Check if limit exceeded
if (count($request_timestamps) >= $max_requests) {
// Sort timestamps to find the oldest one
sort($request_timestamps);
$oldest_request = $request_timestamps[0];
$retry_after = ($oldest_request + $window_seconds) - $now;
return [
'allowed' => false,
'retry_after' => max(1, $retry_after)
];
}
// Add current request timestamp
$request_timestamps[] = $now;
// Save back to cache with TTL equal to the window size
$this->CI->cache->save($cache_key, $request_timestamps, $window_seconds);
return [
'allowed' => true,
'retry_after' => null
];
}
/**
* Get rate limit configuration for an endpoint
*
* @param string $endpoint The API endpoint name
* @return array|null Array with 'requests' and 'window', or null if not configured
*/
protected function get_limit($endpoint) {
if (!is_array($this->rate_limits)) {
return null;
}
// Check for endpoint-specific limit
if (isset($this->rate_limits[$endpoint])) {
return $this->rate_limits[$endpoint];
}
// Check for default limit
if (isset($this->rate_limits['default'])) {
return $this->rate_limits['default'];
}
return null;
}
/**
* Generate identifier for rate limiting
* Uses API key from request, session user ID, or IP address
*
* @return string Unique identifier
*/
protected function generate_identifier() {
$raw_input = json_decode(file_get_contents("php://input"), true);
// Try API key first
if (!empty($raw_input['key'])) {
return 'api_key_' . $raw_input['key'];
}
// Try session user ID
if (!empty($this->CI->session->userdata('user_id'))) {
return 'user_' . $this->CI->session->userdata('user_id');
}
// Fallback to IP address
return 'ip_' . $this->CI->input->ip_address();
}
/**
* Send rate limit exceeded response
*
* @param int $retry_after Seconds until retry is allowed
*/
public function send_limit_exceeded_response($retry_after) {
http_response_code(429);
header('Retry-After: ' . $retry_after);
echo json_encode([
'status' => 'failed',
'reason' => 'Rate limit exceeded. Try again in ' . $retry_after . ' seconds.',
'retry_after' => $retry_after
]);
exit;
}
}

View File

@@ -143,7 +143,7 @@ class Reg1testformat {
$qsorow .= ($newdxcc ? 'N' : '') . ';'; //flag if DXCC is new
$qsorow .= ";\r\n"; //flag for duplicate QSO. Leave empty as Wavelog does not have this.
$qsorow .= "\r\n"; //flag for duplicate QSO. Leave empty as Wavelog does not have this. Do not include a semicolon at the end as this is optional
//add row to overall result
$result['formatted_qso'] .= $qsorow;

View File

@@ -43,7 +43,7 @@ class Sota
// fetches the summit information from SOTA
public function info($summit) {
$url = 'https://api2.sota.org.uk/api/summits/' . $summit;
$url = 'https://api-db2.sota.org.uk/api/summits/' . $summit;
// Let's use cURL instead of file_get_contents
// begin script
@@ -58,9 +58,6 @@ class Sota
$summit_info = curl_exec($ch);
// Close cURL handle
curl_close($ch);
return $summit_info;
}
}

View File

@@ -58,9 +58,6 @@ class Wwff
$summit_info = curl_exec($ch);
// Close cURL handle
curl_close($ch);
return $summit_info;
}
}