Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/config/migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
|
*/

$config['migration_version'] = 270;
$config['migration_version'] = 271;

/*
|--------------------------------------------------------------------------
Expand Down
68 changes: 68 additions & 0 deletions application/migrations/271_add_qrzcall_to_cloudlog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

defined('BASEPATH') or exit('No direct script access allowed');

/*
* Migration: Add QRZCALL.EU support to Cloudlog
*
* Adds two columns to station_profile for storing the user's Personal Access
* Token and real-time upload preference, and two columns to the QSO table
* for tracking upload status.
*/

class Migration_add_qrzcall_to_cloudlog extends CI_Migration
{
public function up()
{
// --- station_profile: QRZCALL.EU Personal Access Token ---
if (!$this->db->field_exists('qrzcallapikey', 'station_profile')) {
$fields = [
'qrzcallapikey varchar(100) DEFAULT NULL',
];
$this->dbforge->add_column('station_profile', $fields);
}

// --- station_profile: real-time upload toggle ---
if (!$this->db->field_exists('qrzcallrealtime', 'station_profile')) {
$fields = [
'qrzcallrealtime tinyint(1) DEFAULT 0',
];
$this->dbforge->add_column('station_profile', $fields);
}

// --- QSO table: upload status (NULL/N = pending, Y = uploaded, M = modified/re-upload) ---
if (!$this->db->field_exists('COL_QRZCALL_QSO_UPLOAD_STATUS', $this->config->item('table_name'))) {
$fields = [
'COL_QRZCALL_QSO_UPLOAD_STATUS varchar(1) DEFAULT NULL',
];
$this->dbforge->add_column($this->config->item('table_name'), $fields);
}

// --- QSO table: timestamp of last successful upload ---
if (!$this->db->field_exists('COL_QRZCALL_QSO_UPLOAD_DATE', $this->config->item('table_name'))) {
$fields = [
'COL_QRZCALL_QSO_UPLOAD_DATE datetime DEFAULT NULL',
];
$this->dbforge->add_column($this->config->item('table_name'), $fields);
}
}

public function down()
{
if ($this->db->field_exists('qrzcallapikey', 'station_profile')) {
$this->dbforge->drop_column('station_profile', 'qrzcallapikey');
}

if ($this->db->field_exists('qrzcallrealtime', 'station_profile')) {
$this->dbforge->drop_column('station_profile', 'qrzcallrealtime');
}

if ($this->db->field_exists('COL_QRZCALL_QSO_UPLOAD_STATUS', $this->config->item('table_name'))) {
$this->dbforge->drop_column($this->config->item('table_name'), 'COL_QRZCALL_QSO_UPLOAD_STATUS');
}

if ($this->db->field_exists('COL_QRZCALL_QSO_UPLOAD_DATE', $this->config->item('table_name'))) {
$this->dbforge->drop_column($this->config->item('table_name'), 'COL_QRZCALL_QSO_UPLOAD_DATE');
}
}
}
124 changes: 123 additions & 1 deletion application/models/Logbook_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,21 @@ function add_qso($data, $skipexport = false)
}
}

$result = ''; // Empty result from previous attempt for safety
$result = $this->exists_qrzcall_api_key($data['station_id']);
// Push QSO to QRZCALL.EU if PAT is set and realtime upload is enabled
if (isset($result->qrzcallapikey) && !empty($result->qrzcallapikey) && $result->qrzcallrealtime == 1) {
$CI = &get_instance();
$CI->load->library('AdifHelper');
$qso = $this->get_qso($last_id, true)->result();

$adif = $CI->adifhelper->getAdifLine($qso[0]);
$upload = $this->push_qso_to_qrzcall($result->qrzcallapikey, $adif);
if ($upload['status'] == 'OK') {
$this->mark_qrzcall_qso_sent($last_id);
}
}

$result = $this->exists_webadif_api_key($data['station_id']);
// Push qso to webadif if apikey is set, and realtime upload is enabled, and we're not importing an adif-file
if (isset($result->webadifapikey) && $result->webadifrealtime == 1) {
Expand Down Expand Up @@ -1079,6 +1094,78 @@ function push_qso_to_qrz($apikey, $adif, $replaceoption = false)
curl_close($ch);
}

/*
* Function uploads a QSO to QRZCALL.EU logbook using the QRZ-compatible endpoint.
* $pat is the user's Personal Access Token (format: pat_xxxxx).
* $adif contains a line with the QSO in the ADIF format.
*/
function push_qso_to_qrzcall($pat, $adif, $replaceoption = false)
{
$url = 'https://api.qrzcall.eu/v1/pub/logbook_api.php';

// Build compliant User-Agent using the shared helper, as push_qso_to_qrz() does
$this->load->helper('useragent');
$ua = cloudlog_user_agent();

$post_data['KEY'] = $pat;
$post_data['ACTION'] = 'INSERT';
$post_data['ADIF'] = $adif;

// OPTION=REPLACE asks QRZCALL.EU to update an existing matching QSO in
// place instead of rejecting it as a duplicate — used to re-sync edited
// QSOs. Mirrors Cloudlog's QRZ.com push_qso_to_qrz().
if ($replaceoption) {
$post_data['OPTION'] = 'REPLACE';
}

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);

$content = curl_exec($ch);

if ($content) {
if (stristr($content, 'RESULT=OK') || stristr($content, 'RESULT=REPLACE')) {
$result['status'] = 'OK';
} elseif (stristr($content, 'RESULT=FAIL') && stristr($content, 'duplicate')) {
// QRZCALL.EU reports the QSO is already in the logbook — treat it as
// success so the QSO is recorded with status 'Y' rather than flagged
// as an upload error.
$result['status'] = 'OK';
$result['duplicate'] = true;
} else {
$result['status'] = 'error';
$result['message'] = $content;
}
} elseif (curl_errno($ch)) {
$result['status'] = 'error';
$result['message'] = 'Curl error: ' . curl_errno($ch);
} else {
$result['status'] = 'error';
$result['message'] = 'Empty response from QRZCALL.EU';
}

curl_close($ch);
return $result;
}

/*
* Function marks a QSO as uploaded to QRZCALL.EU
*/
function mark_qrzcall_qso_sent($primarykey)
{
$data = array(
'COL_QRZCALL_QSO_UPLOAD_DATE' => date("Y-m-d H:i:s", strtotime("now")),
'COL_QRZCALL_QSO_UPLOAD_STATUS' => 'Y',
);
$this->db->where('COL_PRIMARY_KEY', $primarykey);
$this->db->update($this->config->item('table_name'), $data);
}

/*
* Function uploads a QSO to WebADIF consumer with the API given.
* $adif contains a line with the QSO in the ADIF format.
Expand Down Expand Up @@ -1547,7 +1634,24 @@ function edit()

$this->db->where('COL_PRIMARY_KEY', $this->input->post('id'));
$this->db->update($this->config->item('table_name'), $data);


// Real-time re-upload of the edited QSO to QRZCALL.EU. The QSO already
// exists there from the create-time upload, so it is sent with
// OPTION=REPLACE — QRZCALL.EU updates the existing record in place.
$qrzcall_station = $this->exists_qrzcall_api_key($data['station_id']);
if (isset($qrzcall_station->qrzcallapikey) && !empty($qrzcall_station->qrzcallapikey) && $qrzcall_station->qrzcallrealtime == 1) {
$CI = &get_instance();
$CI->load->library('AdifHelper');
$qso = $this->get_qso($this->input->post('id'), true)->result();
if (!empty($qso)) {
$adif = $CI->adifhelper->getAdifLine($qso[0]);
$upload = $this->push_qso_to_qrzcall($qrzcall_station->qrzcallapikey, $adif, true);
if ($upload['status'] == 'OK') {
$this->mark_qrzcall_qso_sent($this->input->post('id'));
}
}
}

// Clear dashboard cache for affected station
$this->clear_dashboard_cache($stationId);

Expand Down Expand Up @@ -2230,6 +2334,24 @@ function get_station_id_with_qrz_api()
}
}

/*
* Function checks if a QRZCALL.EU PAT exists for the given station id
*/
function exists_qrzcall_api_key($station_id)
{
$sql = 'select qrzcallapikey, qrzcallrealtime from station_profile
where station_id = ?';

$query = $this->db->query($sql, $station_id);
$result = $query->row();

if ($result) {
return $result;
} else {
return false;
}
}

/*
* Function returns all the station_id's with HRDLOG Code
*/
Expand Down
4 changes: 4 additions & 0 deletions application/models/Stations.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ function add() {
'webadifapikey' => xss_clean($this->input->post('webadifapikey', true)),
'webadifapiurl' => 'https://qo100dx.club/api',
'webadifrealtime' => xss_clean($this->input->post('webadifrealtime', true)),
'qrzcallapikey' => xss_clean($this->input->post('qrzcallapikey', true)),
'qrzcallrealtime' => xss_clean($this->input->post('qrzcallrealtime', true)),
);

// Insert Records & return insert id //
Expand Down Expand Up @@ -189,6 +191,8 @@ function edit() {
'webadifapikey' => xss_clean($this->input->post('webadifapikey', true)),
'webadifapiurl' => 'https://qo100dx.club/api',
'webadifrealtime' => xss_clean($this->input->post('webadifrealtime', true)),
'qrzcallapikey' => xss_clean($this->input->post('qrzcallapikey', true)),
'qrzcallrealtime' => xss_clean($this->input->post('qrzcallrealtime', true)),
);

$this->db->where('user_id', $this->session->userdata('user_id'));
Expand Down
15 changes: 15 additions & 0 deletions application/views/station_profile/create.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,21 @@ function validateInput(input) {
</div>
</div>

<div class="row">
<div class="mb-3 col-sm-6">
<label for="qrzcallApiKey">QRZCALL.EU Personal Access Token</label> <!-- This does not need Multilanguage Support -->
<input type="text" class="form-control" name="qrzcallapikey" id="qrzcallApiKey" aria-describedby="qrzcallApiKeyHelp" placeholder="pat_xxxxx">
<small id="qrzcallApiKeyHelp" class="form-text text-muted">Generate at <a href="https://qrzcall.eu/" target="_blank">qrzcall.eu</a> → My Profile → Account → API Tokens (Data / Extra tier)</small>
</div>
<div class="mb-3 col-sm-6">
<label for="qrzcallrealtime">QRZCALL.EU real-time upload</label> <!-- This does not need Multilanguage Support -->
<select class="form-select" id="qrzcallrealtime" name="qrzcallrealtime">
<option value="1"><?php echo lang("general_word_yes"); ?></option>
<option value="0" selected><?php echo lang("general_word_no"); ?></option>
</select>
</div>
</div>

<div class="row">
<div class="mb-3 col-sm-6">
<label for="webadifApiKey"> QO-100 Dx Club API Key </label> <!-- This does not need Multilanguage Support -->
Expand Down
22 changes: 22 additions & 0 deletions application/views/station_profile/edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,28 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md">
<div class="card">
<h5 class="card-header">QRZCALL.EU <span class="badge text-bg-warning">Data / Extra</span></h5> <!-- This does not need Multilanguage Support -->
<div class="card-body">
<div class="mb-3">
<label for="qrzcallApiKey">QRZCALL.EU Personal Access Token</label> <!-- This does not need Multilanguage Support -->
<input type="text" class="form-control" name="qrzcallapikey" id="qrzcallApiKey" aria-describedby="qrzcallApiKeyHelp" placeholder="pat_xxxxx" value="<?php if(set_value('qrzcallapikey') != "") { echo set_value('qrzcallapikey'); } else { echo $my_station_profile->qrzcallapikey; } ?>">
<small id="qrzcallApiKeyHelp" class="form-text text-muted">Generate at <a href="https://qrzcall.eu/" target="_blank">qrzcall.eu</a> → My Profile → Account → API Tokens (requires Data or Extra subscription)</small>
</div>
<div class="mb-3">
<label for="qrzcallrealtime">Real-time QSO upload</label> <!-- This does not need Multilanguage Support -->
<select class="form-select" id="qrzcallrealtime" name="qrzcallrealtime">
<option value="1" <?php if ($my_station_profile->qrzcallrealtime == 1) { echo " selected=\"selected\""; } ?>><?php echo lang("general_word_yes"); ?></option>
<option value="0" <?php if ($my_station_profile->qrzcallrealtime == 0) { echo " selected=\"selected\""; } ?>><?php echo lang("general_word_no"); ?></option>
</select>
</div>
Comment on lines +1051 to +1064
</div>
</div>
</div>

</div>
<div class="row">
<div class="col-md">
<div class="card">
Expand Down
44 changes: 44 additions & 0 deletions cypress/e2e/6-qrzcall.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
describe("QRZCALL.EU integration", () => {
beforeEach(() => {
cy.login();
});

it("should show the QRZCALL.EU PAT field on the station create page", () => {
cy.visit("/index.php/station/create");

// The Personal Access Token text input should be present
cy.get('input[name="qrzcallapikey"]')
.should("exist")
.and("be.visible")
.and("have.attr", "placeholder", "pat_xxxxx");

// The real-time upload select should be present with Yes/No options
cy.get('select[name="qrzcallrealtime"]')
.should("exist")
.find("option")
.should("have.length", 2);
});

it("should accept a Personal Access Token in the QRZCALL.EU field", () => {
cy.visit("/index.php/station/create");

const testPat = "pat_cypresstest1234567890";

cy.get('input[name="qrzcallapikey"]')
.scrollIntoView()
.type(testPat, { force: true })
.should("have.value", testPat);

// Real-time upload defaults to "No"
cy.get('select[name="qrzcallrealtime"]')
.find("option:selected")
.should("have.value", "0");

// And can be switched to "Yes" (force: the QRZCALL.EU card can sit
// under the station form's sticky action bar in the test viewport)
cy.get('select[name="qrzcallrealtime"]').select("1", { force: true });
cy.get('select[name="qrzcallrealtime"]')
.find("option:selected")
.should("have.value", "1");
});
});