cloudlog/application/controllers/Api.php
Peter Goodhall 3ca30c161a Remove redundant qso['lng'] assignments
Delete duplicate qso['lng'] assignments in application/controllers/Api.php. Longitude is stored in qso['long'], so removing the extra qso['lng'] assignments (from plot_latlng and dxcc fallback) avoids redundant/conflicting keys and keeps the QSO data consistent.
2026-05-18 22:02:40 +01:00

1632 lines
48 KiB
PHP

<?php if (! defined('BASEPATH')) exit('No direct script access allowed');
class API extends CI_Controller
{
// Do absolutely nothing
function index()
{
echo "nothing to see";
}
function help()
{
$this->load->model('user_model');
// Check if users logged in
if ($this->user_model->validate_session() == 0) {
// user is not logged in
redirect('user/login');
}
$this->load->model('api_model');
$data['api_keys'] = $this->api_model->keys();
$data['page_title'] = "API";
$this->load->view('interface_assets/header', $data);
$this->load->view('api/help');
$this->load->view('interface_assets/footer');
}
function edit($key)
{
$this->load->model('user_model');
// Check if users logged in
if ($this->user_model->validate_session() == 0) {
// user is not logged in
redirect('user/login');
}
$this->load->model('api_model');
$this->load->helper(array('form', 'url'));
$this->load->library('form_validation');
$this->form_validation->set_rules('api_desc', 'API Description', 'required');
$this->form_validation->set_rules('api_key', 'API Key is required do not change this field', 'required');
$data['api_info'] = $this->api_model->key_description($key);
if ($this->form_validation->run() == FALSE) {
$data['page_title'] = "Edit API Description";
$this->load->view('interface_assets/header', $data);
$this->load->view('api/description');
$this->load->view('interface_assets/footer');
} else {
// Success!
$this->api_model->update_key_description($this->input->post('api_key'), $this->input->post('api_desc'));
$this->session->set_flashdata('notice', 'API Key <b>' . $this->input->post('api_key') . "</b> description has been updated.");
redirect('api/help');
}
}
function generate($rights)
{
$this->load->model('user_model');
// Check if users logged in
if ($this->user_model->validate_session() == 0) {
// user is not logged in
redirect('user/login');
}
$this->load->model('api_model');
$data['api_keys'] = $this->api_model->generate_key($rights);
redirect('api/help');
}
function delete($key)
{
$this->load->model('user_model');
// Check if users logged in
if ($this->user_model->validate_session() == 0) {
// user is not logged in
redirect('user/login');
}
$this->load->model('api_model');
$this->api_model->delete_key($key);
$this->session->set_flashdata('notice', 'API Key <b>' . $key . "</b> has been deleted");
redirect('api/help');
}
// Example of authing
function auth($key)
{
$this->load->model('api_model');
header("Content-type: text/xml");
if ($this->api_model->access($key) == "No Key Found" || $this->api_model->access($key) == "Key Disabled") {
echo "<auth>";
echo "<message>Key Invalid - either not found or disabled</message>";
echo "</auth>";
} else {
echo "<auth>";
echo "<status>Valid</status>";
echo "<rights>" . $this->api_model->access($key) . "</rights>";
echo "</auth>";
}
}
function check_auth($key)
{
$this->load->model('api_model');
header("Content-type: text/xml");
if ($this->api_model->access($key) == "No Key Found" || $this->api_model->access($key) == "Key Disabled") {
// set the content type as json
header("Content-type: application/json");
// set the http response code to 401
http_response_code(401);
// return the json with the status as failed
echo json_encode(['status' => 'failed', 'reason' => "missing or invalid api key"]);
} else {
// set the content type as json
header("Content-type: application/json");
// set the http response code to 200
http_response_code(200);
// return the json
echo json_encode(['status' => 'valid', 'rights' => $this->api_model->access($key)]);
}
}
function station_info($key)
{
$this->load->model('api_model');
$this->load->model('stations');
header("Content-type: application/json");
if (substr($this->api_model->access($key), 0, 1) == 'r') { /* Checkpermission for _r_eading */
$this->api_model->update_last_used($key);
$userid = $this->api_model->key_userid($key);
$station_ids = array();
$stations = $this->stations->all_of_user($userid);
foreach ($stations->result() as $row) {
$result['station_id'] = $row->station_id;
$result['station_profile_name'] = $row->station_profile_name;
$result['station_gridsquare'] = $row->station_gridsquare;
$result['station_callsign'] = $row->station_callsign;;
$result['station_active'] = $row->station_active;
array_push($station_ids, $result);
}
echo json_encode($station_ids);
} else {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing or invalid api key"]);
}
}
/**
* Get station logbooks that have public slugs for the API key owner.
* GET /api/logbook_public_slugs/{key}
*/
function logbook_public_slugs($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('logbooks_model');
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'missing or invalid api key']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
$query = $this->logbooks_model->public_slugs_by_user($user_id);
$logbooks = array();
foreach ($query->result() as $row) {
$logbooks[] = array(
'logbook_id' => (int)$row->logbook_id,
'logbook_name' => $row->logbook_name,
'public_slug' => $row->public_slug,
);
}
http_response_code(200);
echo json_encode(array(
'status' => 'success',
'count' => count($logbooks),
'logbooks' => $logbooks,
), JSON_PRETTY_PRINT);
}
/**
* Get all accessible station logbooks (owned + shared) that have public slugs for the API key owner.
* GET /api/logbook_public_slugs_accessible/{key}
*/
function logbook_public_slugs_accessible($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('logbooks_model');
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'missing or invalid api key']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
$query = $this->logbooks_model->public_slugs_accessible_by_user($user_id);
$logbooks = array();
foreach ($query->result() as $row) {
$logbooks[] = array(
'logbook_id' => (int)$row->logbook_id,
'logbook_name' => $row->logbook_name,
'public_slug' => $row->public_slug,
'access_level' => $row->access_level,
);
}
http_response_code(200);
echo json_encode(array(
'status' => 'success',
'count' => count($logbooks),
'logbooks' => $logbooks,
), JSON_PRETTY_PRINT);
}
/*
*
* Function: QSO
* Task: allows passing of ADIF data to Cloudlog
*/
function qso()
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('stations');
$return_msg = array();
$return_count = 0;
// Decode JSON and store
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj === NULL) {
// Decoding not valid try simple www-x-form-urlencoded
$objTmp = file_get_contents("php://input");
parse_str($objTmp, $obj);
if ($obj === NULL) {
echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
die();
}
}
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
die();
}
$userid = $this->api_model->key_userid($obj['key']);
if (!isset($obj['station_profile_id']) || $this->stations->check_station_against_user($obj['station_profile_id'], $userid) == false) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "station id does not belong to the API key owner."]);
die();
}
if ($obj['type'] == "adif" && $obj['string'] != "") {
// Load the logbook model for adding QSO records
$this->load->model('logbook_model');
// Load ADIF Parser
$this->load->library('adif_parser');
// Feed in the ADIF string
$this->adif_parser->feed($obj['string']);
// Create QSO Record
while ($record = $this->adif_parser->get_record()) {
if (count($record) == 0) {
break;
};
if (isset($obj['station_profile_id'])) {
if (isset($record['station_callsign']) && $this->stations->check_station_against_callsign($obj['station_profile_id'], $record['station_callsign']) == false) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "station callsign does not match station callsign in station profile."]);
die();
}
if (!(isset($record['call'])) || (trim($record['call']) == '')) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "QSO Call is empty."]);
die();
}
$this->api_model->update_last_used($obj['key']);
$msg = $this->logbook_model->import($record, $obj['station_profile_id'], NULL, NULL, NULL, NULL, NULL, NULL, false, false, true);
if ($msg == "") {
$return_count++;
} else {
$return_msg[] = $msg;
}
}
};
http_response_code(201);
echo json_encode(['status' => 'created', 'type' => $obj['type'], 'string' => $obj['string'], 'imported_count' => $return_count, 'messages' => $return_msg]);
}
}
// API function to check if a callsign is in the logbook already
function logbook_check_callsign()
{
header('Content-type: application/json');
$this->load->model('api_model');
// Decode JSON and store
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj === NULL) {
echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
return;
}
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
return;
}
if (!isset($obj['logbook_public_slug']) || !isset($obj['callsign'])) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing fields"]);
return;
}
if ($obj['logbook_public_slug'] != "" && $obj['callsign'] != "") {
$logbook_slug = $obj['logbook_public_slug'];
$callsign = $obj['callsign'];
// If $obj['band'] exists
if (isset($obj['band'])) {
$band = $obj['band'];
} else {
$band = null;
}
$this->load->model('logbooks_model');
if ($this->logbooks_model->public_slug_exists($logbook_slug)) {
$logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($logbook_slug);
if ($logbook_id != false) {
// Get associated station locations for mysql queries
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
if (!$logbooks_locations_array) {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "Empty Logbook"]);
die();
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => $logbook_slug . " has no associated station locations"]);
die();
}
// Search Logbook for callsign
$this->load->model('logbook_model');
$result = $this->logbook_model->check_if_callsign_worked_in_logbook($callsign, $logbooks_locations_array, $band);
http_response_code(201);
if ($result > 0) {
echo json_encode(['callsign' => $callsign, 'result' => 'Found']);
} else {
echo json_encode(['callsign' => $callsign, 'result' => 'Not Found']);
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "logbook not found"]);
die();
}
}
}
// API function to check if a grid is in the logbook already
function logbook_check_grid()
{
header('Content-type: application/json');
$this->load->model('api_model');
// Decode JSON and store
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj === NULL) {
echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
}
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
}
if (!isset($obj['logbook_public_slug']) || !isset($obj['grid'])) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing fields"]);
return;
}
if ($obj['logbook_public_slug'] != "" && $obj['grid'] != "") {
$logbook_slug = $obj['logbook_public_slug'];
$grid = $obj['grid'];
// If $obj['band'] exists
if (isset($obj['band'])) {
$band = $obj['band'];
} else {
$band = null;
}
$this->load->model('logbooks_model');
if ($this->logbooks_model->public_slug_exists($logbook_slug)) {
$logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($logbook_slug);
if ($logbook_id != false) {
// Get associated station locations for mysql queries
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
if (!$logbooks_locations_array) {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "Empty Logbook"]);
die();
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => $logbook_slug . " has no associated station locations"]);
die();
}
// Search Logbook for callsign
$this->load->model('logbook_model');
$result = $this->logbook_model->check_if_grid_worked_in_logbook($grid, $logbooks_locations_array, $band);
http_response_code(201);
if ($result > 0) {
echo json_encode(['gridsquare' => strtoupper($grid), 'result' => 'Found']);
} else {
echo json_encode(['gridsquare' => strtoupper($grid), 'result' => 'Not Found']);
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "logbook not found"]);
die();
}
}
}
/**
* Check if a country has been worked before and confirmed in various ways
*
* This API endpoint checks if a specific country (derived from callsign) has been
* worked before in a given logbook, and whether it has been confirmed via different
* confirmation methods (QSL, LoTW, eQSL, QRZ).
*
* @api POST /api/logbook_check_country
* @header Content-Type application/json
*
* @param string key Required. API authentication key
* @param string logbook_public_slug Required. Public slug identifier for the logbook
* @param string callsign Required. Callsign to lookup country for
* @param string type Optional. Type of contact ("sat" for satellite, empty for regular)
* @param string band Optional. Amateur radio band (required for non-satellite contacts)
* @param string mode Optional. Amateur radio mode (required for non-satellite contacts)
*
* @return json Returns JSON object with:
* - workedBefore: boolean indicating if country was worked before
* - confirmed: object with confirmation status for qsl, lotw, eqsl, qrz
*
* @throws 401 Unauthorized - Missing or invalid API key, missing required fields
* @throws 404 Not Found - Logbook not found or empty logbook
* @throws 400 Bad Request - Invalid JSON format
*
* @example
* Request:
* {
* "key": "your-api-key",
* "logbook_public_slug": "my-logbook",
* "callsign": "W1AW",
* "band": "20M",
* "mode": "SSB"
* }
*
* Response:
* {
* "workedBefore": true,
* "confirmed": {
* "qsl": true,
* "lotw": false,
* "eqsl": false,
* "qrz": false
* }
* }
*/
function logbook_check_country()
{
header('Content-type: application/json');
$this->load->model('api_model');
// Decode JSON and store
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj === NULL) {
echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
return;
}
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
return;
}
if (!isset($obj['logbook_public_slug']) || !isset($obj['callsign'])) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing fields"]);
return;
}
// Load models
$this->load->model('logbook_model');
$this->load->model('logbooks_model');
$date = date("Y-m-d");
$callsign = $obj['callsign'];
$logbook_slug = $obj['logbook_public_slug'];
$type = isset($obj['type']) ? $obj['type'] : '';
$band = isset($obj['band']) ? $obj['band'] : '';
$mode = isset($obj['mode']) ? $obj['mode'] : '';
$callsign_dxcc_lookup = $this->logbook_model->dxcc_lookup($callsign, $date);
$country = $callsign_dxcc_lookup['entity'];
$return = [
"workedBefore" => false,
"confirmed" => [
"qsl" => false,
"lotw" => false,
"eqsl" => false,
"qrz" => false
]
];
if ($this->logbooks_model->public_slug_exists($logbook_slug)) {
$logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($logbook_slug);
if ($logbook_id != false) {
// Get associated station locations for mysql queries
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
if (!$logbooks_locations_array) {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "Empty Logbook"]);
die();
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => $logbook_slug . " has no associated station locations"]);
die();
}
if (!empty($logbooks_locations_array)) {
if ($type == "sat") {
$this->db->where('COL_PROP_MODE', 'SAT');
} else {
$this->db->where('COL_MODE', $this->logbook_model->get_main_mode_from_mode($mode));
$this->db->where('COL_BAND', $band);
$this->db->where('COL_PROP_MODE !=', 'SAT');
}
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$query = $this->db->get($this->config->item('table_name'), 1, 0);
foreach ($query->result() as $workedBeforeRow) {
$return['workedBefore'] = true;
}
// Check each confirmation type separately
$confirmation_types = [
'qsl' => "COL_QSL_RCVD='Y'",
'lotw' => "COL_LOTW_QSL_RCVD='Y'",
'eqsl' => "COL_EQSL_QSL_RCVD='Y'",
'qrz' => "COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y'"
];
foreach ($confirmation_types as $type_key => $where_clause) {
if ($type == "SAT") {
$this->db->where('COL_PROP_MODE', 'SAT');
} else {
$this->db->where('COL_MODE', $this->logbook_model->get_main_mode_from_mode($mode));
$this->db->where('COL_BAND', $band);
$this->db->where('COL_PROP_MODE !=', 'SAT');
}
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$this->db->where($where_clause);
$query = $this->db->get($this->config->item('table_name'), 1, 0);
if ($query->num_rows() > 0) {
$return['confirmed'][$type_key] = true;
}
}
http_response_code(201);
echo json_encode($return, JSON_PRETTY_PRINT);
} else {
http_response_code(201);
echo json_encode($return, JSON_PRETTY_PRINT);
}
} else {
// Logbook not found
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "logbook not found"]);
die();
}
}
/**
* Check worked and confirmation status for callsign and country.
*
* @api POST /api/logbook_worked_status
* @header Content-Type application/json
*
* Required fields:
* - key
* - logbook_public_slug
* - callsign
*
* Optional fields:
* - band
* - mode
* - gridsquare
*/
function logbook_worked_status()
{
header('Content-type: application/json');
$this->load->model('api_model');
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj === NULL) {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
return;
}
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
return;
}
if (!isset($obj['logbook_public_slug']) || !isset($obj['callsign'])) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing fields"]);
return;
}
$this->load->model('logbook_model');
$this->load->model('logbooks_model');
$logbook_slug = trim($obj['logbook_public_slug']);
$callsign = strtoupper(trim($obj['callsign']));
$band = isset($obj['band']) && trim($obj['band']) !== '' ? strtoupper(trim($obj['band'])) : null;
$mode = isset($obj['mode']) && trim($obj['mode']) !== '' ? strtoupper(trim($obj['mode'])) : null;
$gridsquare = isset($obj['gridsquare']) && trim($obj['gridsquare']) !== '' ? strtoupper(trim($obj['gridsquare'])) : null;
$date = date("Y-m-d");
$callsign_dxcc_lookup = $this->logbook_model->dxcc_lookup($callsign, $date);
$country = isset($callsign_dxcc_lookup['entity']) ? $callsign_dxcc_lookup['entity'] : '';
if (!$this->logbooks_model->public_slug_exists($logbook_slug)) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "logbook not found"]);
return;
}
$logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($logbook_slug);
if ($logbook_id == false) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => $logbook_slug . " has no associated station locations"]);
return;
}
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
if (!$logbooks_locations_array) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => "Empty Logbook"]);
return;
}
$return = [
'callsign' => $callsign,
'country' => $country,
'band' => $band,
'mode' => $mode,
'gridsquare' => $gridsquare,
'worked' => [
'callsign_overall' => false,
'callsign_band' => $band !== null ? false : null,
'country_overall' => false,
'country_band' => $band !== null ? false : null,
'grid_overall' => $gridsquare !== null ? false : null,
'grid_band' => ($gridsquare !== null && $band !== null) ? false : null,
],
'confirmed' => [
'callsign' => [
'lotw' => false,
'qsl' => false,
'any' => false,
],
'country' => [
'lotw' => false,
'qsl' => false,
'any' => false,
],
],
];
$table_name = $this->config->item('table_name');
$main_mode = $mode !== null ? $this->logbook_model->get_main_mode_from_mode($mode) : null;
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_CALL', $callsign);
$callsign_overall_query = $this->db->get($table_name, 1, 0);
$return['worked']['callsign_overall'] = $callsign_overall_query->num_rows() > 0;
if ($band !== null) {
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_CALL', $callsign);
$this->db->where('COL_BAND', $band);
if ($main_mode !== null) {
$this->db->where('COL_MODE', $main_mode);
}
$callsign_band_query = $this->db->get($table_name, 1, 0);
$return['worked']['callsign_band'] = $callsign_band_query->num_rows() > 0;
}
if ($country !== '') {
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$country_overall_query = $this->db->get($table_name, 1, 0);
$return['worked']['country_overall'] = $country_overall_query->num_rows() > 0;
if ($band !== null) {
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$this->db->where('COL_BAND', $band);
if ($main_mode !== null) {
$this->db->where('COL_MODE', $main_mode);
}
$country_band_query = $this->db->get($table_name, 1, 0);
$return['worked']['country_band'] = $country_band_query->num_rows() > 0;
}
}
if ($gridsquare !== null) {
$grid_overall_result = $this->logbook_model->check_if_grid_worked_in_logbook($gridsquare, $logbooks_locations_array, null);
$return['worked']['grid_overall'] = $grid_overall_result > 0;
if ($band !== null) {
$grid_band_result = $this->logbook_model->check_if_grid_worked_in_logbook($gridsquare, $logbooks_locations_array, $band);
$return['worked']['grid_band'] = $grid_band_result > 0;
}
}
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_CALL', $callsign);
$this->db->where("COL_LOTW_QSL_RCVD='Y'");
$callsign_lotw_query = $this->db->get($table_name, 1, 0);
$return['confirmed']['callsign']['lotw'] = $callsign_lotw_query->num_rows() > 0;
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_CALL', $callsign);
$this->db->where("COL_QSL_RCVD='Y'");
$callsign_qsl_query = $this->db->get($table_name, 1, 0);
$return['confirmed']['callsign']['qsl'] = $callsign_qsl_query->num_rows() > 0;
$return['confirmed']['callsign']['any'] = $return['confirmed']['callsign']['lotw'] || $return['confirmed']['callsign']['qsl'];
if ($country !== '') {
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$this->db->where("COL_LOTW_QSL_RCVD='Y'");
$country_lotw_query = $this->db->get($table_name, 1, 0);
$return['confirmed']['country']['lotw'] = $country_lotw_query->num_rows() > 0;
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_COUNTRY', urldecode($country));
$this->db->where("COL_QSL_RCVD='Y'");
$country_qsl_query = $this->db->get($table_name, 1, 0);
$return['confirmed']['country']['qsl'] = $country_qsl_query->num_rows() > 0;
$return['confirmed']['country']['any'] = $return['confirmed']['country']['lotw'] || $return['confirmed']['country']['qsl'];
}
http_response_code(200);
echo json_encode($return, JSON_PRETTY_PRINT);
}
/* ENDPOINT for Rig Control */
function radio()
{
header('Content-type: application/json');
$this->load->model('api_model');
//$json = '{"radio":"FT-950","frequency":14075,"mode":"SSB","timestamp":"2012/04/07 16:47"}';
$this->load->model('cat');
//var_dump(file_get_contents("php://input"), true);
// Decode JSON and store
$obj = json_decode(file_get_contents("php://input"), true);
if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
die();
}
$this->api_model->update_last_used($obj['key']);
$user_id = $this->api_model->key_userid($obj['key']);
// Store Result to Database
$this->cat->update($obj, $user_id);
// Return Message
$arr = array('status' => 'success');
echo json_encode($arr);
}
/*
* Radio Commands API Functions for Aurora Desktop App
*/
/**
* Get pending radio commands
* GET /api/radio_commands_pending/{key}
* GET /api/radio_commands_pending_by_name/{key}/{radio_name}
*/
function radio_commands_pending($key, $radio_id = null)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
if ($radio_id) {
// Get commands for specific radio by ID
$commands = $this->cat->get_pending_commands($radio_id, $user_id);
} else {
// Get all pending commands for user
$commands = $this->cat->get_all_pending_commands($user_id);
}
// Debug information
$debug_info = array(
'user_id' => $user_id,
'radio_id' => $radio_id,
'current_time' => date('Y-m-d H:i:s'),
'total_commands_in_db' => $this->db->where('user_id', $user_id)->count_all_results('radio_commands'),
'pending_commands_in_db' => $this->db->where('user_id', $user_id)->where('status', 'PENDING')->count_all_results('radio_commands')
);
echo json_encode([
'status' => 'success',
'commands' => $commands,
'count' => count($commands),
'debug' => $debug_info
]);
}
/**
* Get pending radio commands by radio name (for Aurora)
* GET /api/radio_commands_pending_by_name/{key}/{radio_name}
*/
function radio_commands_pending_by_name($key, $radio_name = null)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
// Clean up expired commands for this user
$cleaned = $this->cat->cleanup_expired_commands($user_id);
if ($radio_name) {
// URL decode the radio name in case it has spaces/special chars
$decoded_radio_name = urldecode($radio_name);
// Additional cleanup - remove any extra whitespace
$decoded_radio_name = trim($decoded_radio_name);
// Debug: Check if radio exists in cat table for this user
$radio_check = $this->db->select('id, radio, user_id')
->where('radio', $decoded_radio_name)
->where('user_id', $user_id)
->get('cat');
// Debug: Check all radios for this user
$all_user_radios = $this->db->select('id, radio')
->where('user_id', $user_id)
->get('cat');
// Debug: Check if any commands exist for this user
$all_commands = $this->db->select('id, radio_id, radio_name, command_type, status, created_at, expires_at')
->where('user_id', $user_id)
->get('radio_commands');
// Get commands for specific radio by name
$commands = $this->cat->get_pending_commands_by_name($decoded_radio_name, $user_id);
$debug_info = array(
'user_id' => $user_id,
'decoded_radio_name' => $decoded_radio_name,
'radio_found_in_cat' => $radio_check->num_rows() > 0,
'radio_details' => $radio_check->num_rows() > 0 ? $radio_check->row_array() : null,
'all_user_radios' => $all_user_radios->result_array(),
'all_user_commands' => $all_commands->result_array(),
'current_time' => date('Y-m-d H:i:s')
);
} else {
// Get all pending commands for user
$commands = $this->cat->get_all_pending_commands($user_id);
$decoded_radio_name = null;
$debug_info = array('user_id' => $user_id);
}
echo json_encode([
'status' => 'success',
'commands' => $commands,
'count' => count($commands),
'radio_name' => $decoded_radio_name,
'original_param' => $radio_name,
'expired_cleaned' => $cleaned
]);
}
/**
* Update radio command status
* POST /api/radio_commands/update_status/{key}
* Body: {"command_id": 123, "status": "COMPLETED", "error_message": "optional"}
*/
function radio_commands_update_status($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
// Get JSON input
$input = json_decode(file_get_contents("php://input"), true);
if (!isset($input['command_id']) || !isset($input['status'])) {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => 'missing command_id or status']);
return;
}
$command_id = $input['command_id'];
$status = $input['status'];
$error_message = isset($input['error_message']) ? $input['error_message'] : null;
// Validate status
$valid_statuses = ['PROCESSING', 'COMPLETED', 'FAILED'];
if (!in_array($status, $valid_statuses)) {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => 'invalid status']);
return;
}
// Verify command belongs to user
$command = $this->cat->get_command($command_id, $user_id);
if (!$command) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'command not found']);
return;
}
// Update command status
$result = $this->cat->update_command_status($command_id, $status, $error_message);
if ($result) {
echo json_encode(['status' => 'success', 'updated' => true]);
} else {
http_response_code(500);
echo json_encode(['status' => 'failed', 'reason' => 'update failed']);
}
}
/**
* Get radio command by ID
* GET /api/radio_commands/get/{key}/{command_id}
*/
function radio_commands_get($key, $command_id)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
$command = $this->cat->get_command($command_id, $user_id);
if ($command) {
echo json_encode([
'status' => 'success',
'command' => $command
]);
} else {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'command not found']);
}
}
/**
* Queue a new radio command (for testing or external apps)
* POST /api/radio_commands_queue/{key}
* Body: {"radio_id": 1, "command_type": "SET_FREQ", "frequency": 14074000, "mode": "USB"}
* OR: {"radio_name": "FT-991A", "command_type": "SET_FREQ", "frequency": 14074000, "mode": "USB"}
*/
function radio_commands_queue($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
// Get JSON input
$input = json_decode(file_get_contents("php://input"), true);
if (!isset($input['command_type'])) {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => 'missing command_type']);
return;
}
$radio_id = null;
// Check if radio_name is provided instead of radio_id
if (isset($input['radio_name'])) {
// Get radio ID from radio name
$radio_query = $this->db->select('id')
->where('radio', $input['radio_name'])
->where('user_id', $user_id)
->get('cat');
if ($radio_query->num_rows() == 0) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'radio not found']);
return;
}
$radio_id = $radio_query->row()->id;
} elseif (isset($input['radio_id'])) {
$radio_id = $input['radio_id'];
} else {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => 'missing radio_id or radio_name']);
return;
}
// Prepare command data
$command_data = array(
'radio_id' => $radio_id,
'user_id' => $user_id,
'command_type' => $input['command_type'],
'expires_at' => date('Y-m-d H:i:s', strtotime('+30 minutes'))
);
// Add optional parameters
if (isset($input['frequency'])) $command_data['frequency'] = $input['frequency'];
if (isset($input['mode'])) $command_data['mode'] = $input['mode'];
if (isset($input['vfo'])) $command_data['vfo'] = $input['vfo'];
if (isset($input['power'])) $command_data['power'] = $input['power'];
if (isset($input['station_id'])) $command_data['station_id'] = $input['station_id'];
$command_id = $this->cat->queue_command($command_data);
if ($command_id) {
echo json_encode([
'status' => 'success',
'command_id' => $command_id,
'message' => 'Command queued successfully'
]);
} else {
http_response_code(500);
echo json_encode(['status' => 'failed', 'reason' => 'failed to queue command']);
}
}
/**
* Get all pending radio commands for all user's radios (optimized for Aurora)
* GET /api/radio_commands_all_pending/{key}
*/
function radio_commands_all_pending($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
// Clean up expired commands for this user
$cleaned = $this->cat->cleanup_expired_commands($user_id);
// Get all pending commands for all user's radios
$commands = $this->cat->get_all_pending_commands($user_id);
// Group commands by radio for easier processing
$commands_by_radio = array();
$radio_names = array();
foreach ($commands as $command) {
$radio_name = $command['radio_name'];
if (!isset($commands_by_radio[$radio_name])) {
$commands_by_radio[$radio_name] = array();
$radio_names[] = $radio_name;
}
$commands_by_radio[$radio_name][] = $command;
}
echo json_encode([
'status' => 'success',
'commands' => $commands,
'commands_by_radio' => $commands_by_radio,
'radio_names' => $radio_names,
'total_commands' => count($commands),
'radios_with_commands' => count($commands_by_radio),
'expired_cleaned' => $cleaned,
'timestamp' => date('Y-m-d H:i:s')
]);
}
/**
* Cleanup expired radio commands (maintenance endpoint)
* POST /api/radio_commands_cleanup/{key}
* Body: {"cleanup_old": true, "days_old": 7} (optional parameters)
*/
function radio_commands_cleanup($key)
{
header('Content-type: application/json');
$this->load->model('api_model');
$this->load->model('cat');
// Check API key
if (!$key || $this->api_model->authorize($key) == 0) {
http_response_code(401);
echo json_encode(['status' => 'failed', 'reason' => 'unauthorized']);
return;
}
$this->api_model->update_last_used($key);
$user_id = $this->api_model->key_userid($key);
// Get optional JSON input
$input = json_decode(file_get_contents("php://input"), true);
$cleanup_old = isset($input['cleanup_old']) ? $input['cleanup_old'] : false;
$days_old = isset($input['days_old']) ? intval($input['days_old']) : 7;
// Clean up expired commands
$expired_cleaned = $this->cat->cleanup_expired_commands($user_id);
$old_cleaned = 0;
if ($cleanup_old) {
$old_cleaned = $this->cat->cleanup_old_commands($days_old, $user_id);
}
echo json_encode([
'status' => 'success',
'expired_commands_cleaned' => $expired_cleaned,
'old_commands_cleaned' => $old_cleaned,
'cleanup_old_enabled' => $cleanup_old,
'days_old_threshold' => $days_old,
'timestamp' => date('Y-m-d H:i:s')
]);
}
/*
*
* Stats API function calls
*
*/
function statistics($key = null)
{
header('Content-type: application/json');
$this->load->model('logbook_model');
$data['todays_qsos'] = $this->logbook_model->todays_qsos(null, $key);
$data['total_qsos'] = $this->logbook_model->total_qsos(null, $key);
$data['month_qsos'] = $this->logbook_model->month_qsos(null, $key);
$data['year_qsos'] = $this->logbook_model->year_qsos(null, $key);
http_response_code(201);
echo json_encode(['Today' => $data['todays_qsos'], 'total_qsos' => $data['total_qsos'], 'month_qsos' => $data['month_qsos'], 'year_qsos' => $data['year_qsos']]);
}
function lookup()
{
// start benchmarking
$this->output->enable_profiler(TRUE);
/*
*
* Callsign lookup function for Cloudlogs logging page or thirdparty systems
* which want to show previous QSO data on their system.
*
* TODO
* - Local data make one database call ONLY
* - Add eQSL status
* - Add Callbook returned data
* - Add QSO before data array
* - Add options for checking based on band/mode/sat
*
*/
// Make sure users logged in
$this->load->model('user_model');
if (!$this->user_model->authorize($this->config->item('auth_mode'))) {
return;
}
$this->load->model("logbook_model");
$date = date("Y-m-d");
// Return Array
$return = [
"callsign" => "",
"dxcc" => false,
"dxcc_lat" => "",
"dxcc_long" => "",
"dxcc_cqz" => "",
"name" => "",
"gridsquare" => "",
"location" => "",
"iota_ref" => "",
"state" => "",
"us_county" => "",
"qsl_manager" => "",
"bearing" => "",
"workedBefore" => false,
"lotw_member" => false,
"suffix_slash" => "", // Suffix Slash aka Portable
];
/*
*
* Handle POST data being sent to check lookups
*
*/
$raw_input = json_decode(file_get_contents("php://input"), true);
$lookup_callsign = strtoupper($raw_input['callsign']);
/*
*
* Handle Callsign field
*
*/
$return['callsign'] = $lookup_callsign;
/*
*
* Lookup DXCC and Suffix information
*
*/
$callsign_dxcc_lookup = $this->logbook_model->dxcc_lookup($lookup_callsign, $date);
$last_slash_pos = strrpos($lookup_callsign, '/');
if (isset($last_slash_pos) && $last_slash_pos > 4) {
$suffix_slash = $last_slash_pos === false ? $lookup_callsign : substr($lookup_callsign, $last_slash_pos + 1);
switch ($suffix_slash) {
case "P":
$suffix_slash_item = "Portable";
break;
case "M":
$suffix_slash_item = "Mobile";
case "MM":
$suffix_slash_item = "Maritime Mobile";
break;
default:
// If its not one of the above suffix slashes its likely dxcc
$ans2 = $this->logbook_model->dxcc_lookup($suffix_slash, $date);
$suffix_slash_item = null;
}
$return['suffix_slash'] = $suffix_slash_item;
}
// If the final slash is a DXCC then find it!
if (isset($ans2['call'])) {
$return['dxcc'] = $ans2['entity'];
$return['dxcc_lat'] = $ans2['lat'];
$return['dxcc_long'] = $ans2['long'];
$return['dxcc_cqz'] = $ans2['cqz'];
} else {
$return['dxcc'] = $callsign_dxcc_lookup['entity'];
$return['dxcc_lat'] = $callsign_dxcc_lookup['lat'];
$return['dxcc_long'] = $callsign_dxcc_lookup['long'];
$return['dxcc_cqz'] = $callsign_dxcc_lookup['cqz'];
}
/*
*
* Pool any local data we have for a callsign
*
*/
$call_lookup_results = $this->logbook_model->call_lookup_result($lookup_callsign);
if ($call_lookup_results != null) {
$return['name'] = $call_lookup_results->COL_NAME;
$return['gridsquare'] = $call_lookup_results->COL_GRIDSQUARE;
$return['location'] = $call_lookup_results->COL_QTH;
$return['iota_ref'] = $call_lookup_results->COL_IOTA;
$return['qsl_manager'] = $call_lookup_results->COL_QSL_VIA;
$return['state'] = $call_lookup_results->COL_STATE;
$return['us_county'] = $call_lookup_results->COL_CNTY;
if ($return['gridsquare'] != "") {
$return['latlng'] = $this->qralatlng($return['gridsquare']);
}
}
/*
*
* Check if callsign is active on LoTW
*
*/
/*
*
* Output Returned data
*
*/
echo json_encode($return, JSON_PRETTY_PRINT);
return;
// End benchmarking
$this->output->enable_profiler(FALSE);
}
function qralatlng($qra)
{
$this->load->library('Qra');
$latlng = $this->qra->qra2latlong($qra);
return $latlng;
}
/**
* API endpoint to get recent QSOs from a public logbook
*
* @api GET /api/recent_qsos/{public_slug}/{limit}
*
* @param string public_slug Required. Public slug identifier for the logbook
* @param int limit Optional. Number of QSOs to return (default: 10, max: 50)
*
* @return json Returns JSON array with recent QSO data or error message
*
* @throws 404 Not Found - Logbook not found or empty logbook
* @throws 400 Bad Request - Invalid limit parameter
*
* @example
* Request: GET /api/recent_qsos/my-public-logbook/5
*
* Response:
* {
* "qsos": [
* {
* "date": "2024-01-15",
* "time": "14:30",
* "callsign": "W1AW",
* "mode": "SSB",
* "band": "20M",
* "rst_sent": "59",
* "rst_rcvd": "59"
* }
* ],
* "count": 1,
* "logbook_slug": "my-public-logbook"
* }
*/
function recent_qsos($public_slug = null, $limit = 10)
{
header('Content-type: application/json');
// Validate and sanitize $limit
if (!is_numeric($limit)) {
$limit = 10; // Default to 10 if not numeric
} else {
$limit = intval($limit);
if ($limit < 1) {
$limit = 1; // Minimum limit of 1
} elseif ($limit > 50) {
$limit = 50; // Maximum limit of 50
}
}
if ($public_slug == null) {
http_response_code(400);
echo json_encode(['status' => 'failed', 'reason' => 'missing public_slug parameter']);
return;
}
$this->load->model('logbooks_model');
$this->load->model('logbook_model');
if ($this->logbooks_model->public_slug_exists($public_slug)) {
$logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($public_slug);
if ($logbook_id != false) {
// Get associated station locations for mysql queries
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
if (!$logbooks_locations_array) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'Empty Logbook']);
return;
}
// Get recent QSOs using existing method
$recent_qsos_query = $this->logbook_model->get_last_qsos($limit, $logbooks_locations_array);
if ($recent_qsos_query == null) {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'No QSOs found']);
return;
}
// Format the data for JSON response
$qsos = array();
foreach ($recent_qsos_query->result() as $row) {
$qso = array(
'date' => date('Y-m-d', strtotime($row->COL_TIME_ON)),
'time' => date('H:i', strtotime($row->COL_TIME_ON)),
'callsign' => strtoupper($row->COL_CALL),
'mode' => $row->COL_SUBMODE ? $row->COL_SUBMODE : $row->COL_MODE,
'band' => $row->COL_SAT_NAME ? $row->COL_SAT_NAME : $row->COL_BAND,
'rst_sent' => $row->COL_RST_SENT,
'rst_rcvd' => $row->COL_RST_RCVD
);
// Add optional fields if they exist
if ($row->COL_STX_STRING) {
$qso['stx_string'] = $row->COL_STX_STRING;
}
if ($row->COL_SRX_STRING) {
$qso['srx_string'] = $row->COL_SRX_STRING;
}
if ($row->COL_VUCC_GRIDS) {
$qso['gridsquare'] = $row->COL_VUCC_GRIDS;
} elseif ($row->COL_GRIDSQUARE) {
$qso['gridsquare'] = $row->COL_GRIDSQUARE;
}
if ($row->COL_QTH) {
$qso['qth'] = $row->COL_QTH;
}
if ($row->COL_NAME) {
$qso['name'] = $row->COL_NAME;
}
// Prefer grid-derived coordinates for plotting, including VUCC multi-grid line/corner records.
$plot_latlng = false;
if (!empty($row->COL_VUCC_GRIDS)) {
$plot_latlng = $this->qralatlng($row->COL_VUCC_GRIDS);
} elseif (!empty($row->COL_GRIDSQUARE)) {
$plot_latlng = $this->qralatlng($row->COL_GRIDSQUARE);
}
if (is_array($plot_latlng) && count($plot_latlng) === 2) {
$qso['lat'] = $plot_latlng[0];
$qso['long'] = $plot_latlng[1];
}
$dxcc = $this->logbook_model->check_dxcc_table(strtoupper(trim(strtoupper($row->COL_CALL))), $row->COL_TIME_ON);
if (empty($dxcc[0])) {
$dxcc_id = null;
} else {
$qso['country'] = $dxcc[1];
if (!isset($qso['lat']) || !isset($qso['long'])) {
$qso['lat'] = $dxcc[4];
$qso['long'] = $dxcc[5];
}
}
$qsos[] = $qso;
}
http_response_code(200);
echo json_encode([
'qsos' => $qsos,
'count' => count($qsos),
'logbook_slug' => $public_slug
], JSON_PRETTY_PRINT);
} else {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => $public_slug . ' has no associated station locations']);
}
} else {
http_response_code(404);
echo json_encode(['status' => 'failed', 'reason' => 'logbook not found']);
}
}
}