mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
Merge remote-tracking branch 'upstream/dev' into cache_buster
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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 != "") {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
125
application/libraries/DxclusterCache.php
Normal file
125
application/libraries/DxclusterCache.php
Normal 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);
|
||||
}
|
||||
}
|
||||
422
application/libraries/EqslBulkDownloader.php
Normal file
422
application/libraries/EqslBulkDownloader.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
172
application/libraries/GarbageCollector.php
Normal file
172
application/libraries/GarbageCollector.php
Normal 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]));
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
305
application/libraries/Geojson.php
Normal file
305
application/libraries/Geojson.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
67
application/libraries/Mh.php
Normal file
67
application/libraries/Mh.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,6 @@ class Pota
|
||||
|
||||
$summit_info = curl_exec($ch);
|
||||
|
||||
// Close cURL handle
|
||||
curl_close($ch);
|
||||
|
||||
return $summit_info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
182
application/libraries/Rate_limit.php
Normal file
182
application/libraries/Rate_limit.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,6 @@ class Wwff
|
||||
|
||||
$summit_info = curl_exec($ch);
|
||||
|
||||
// Close cURL handle
|
||||
curl_close($ch);
|
||||
|
||||
return $summit_info;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user