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
39 changes: 38 additions & 1 deletion application/controllers/Logbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -1005,9 +1005,24 @@ function partial($id)
if (isset($callsign['callsign']['error'])) {
$callsign['error'] = $callsign['callsign']['error'];
}
} elseif ($this->session->userdata('callbook_type') == "QRZCALL") {
// Lookup using QRZCALL.EU — stateless PAT auth, no session-key dance
$this->load->library('qrzcall');
$this->load->library('encryption');

$token = $this->encryption->decrypt($this->session->userdata('callbook_password'));
$callsign['callsign'] = $this->qrzcall->search($id, $token, $this->config->item('use_fullname'));

if (isset($callsign['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($callsign['callsign']['dxcc']);
if (is_array($entity) && isset($entity['name'])) {
$callsign['callsign']['dxcc_name'] = $entity['name'];
}
}
} else {
// No callbook type set, return error message
$callsign['error'] = 'Online callbook not configured. Go to <a href="' . site_url('user/edit/' . $this->session->userdata('user_id')) . '" class="alert-link">Account Settings</a> and select either QRZ or HamQTH in the "Callbook" section.';
$callsign['error'] = 'Online callbook not configured. Go to <a href="' . site_url('user/edit/' . $this->session->userdata('user_id')) . '" class="alert-link">Account Settings</a> and select QRZ, HamQTH or QRZCALL.EU in the "Callbook" section.';
}

if (isset($callsign['callsign']['gridsquare'])) {
Expand Down Expand Up @@ -1116,6 +1131,28 @@ function search_result($id = "", $id2 = "")
$this->session->set_userdata('hamqth_session_key', $hamqth_session_key);
$data['callsign'] = $this->hamqth->search($fixedid, $this->session->userdata('hamqth_session_key'));
}
if (isset($data['callsign']['gridsquare'])) {
$this->load->model('logbook_model');
$data['grid_worked'] = $this->logbook_model->check_if_grid_worked_in_logbook(strtoupper(substr($data['callsign']['gridsquare'], 0, 4)), 0, $this->session->userdata('user_default_band'));
}
if (isset($data['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($data['callsign']['dxcc']);
if (is_array($entity) && isset($entity['name'])) {
$data['callsign']['dxcc_name'] = $entity['name'];
}
}
if (isset($data['callsign']['error'])) {
$data['error'] = $data['callsign']['error'];
}
} elseif ($this->session->userdata('callbook_type') == "QRZCALL") {
// Lookup using QRZCALL.EU — stateless PAT auth, no session-key dance
$this->load->library('qrzcall');
$this->load->library('encryption');

$token = $this->encryption->decrypt($this->session->userdata('callbook_password'));
$data['callsign'] = $this->qrzcall->search($fixedid, $token, $this->config->item('use_fullname'));

if (isset($data['callsign']['gridsquare'])) {
$this->load->model('logbook_model');
$data['grid_worked'] = $this->logbook_model->check_if_grid_worked_in_logbook(strtoupper(substr($data['callsign']['gridsquare'], 0, 4)), 0, $this->session->userdata('user_default_band'));
Expand Down
164 changes: 164 additions & 0 deletions application/libraries/Qrzcall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/*
Controls the interaction with the QRZCALL.EU Premium XML API.

QRZCALL.EU is a QRZ-compatible callsign database. Its XML feed
(/v1/pub/callsign_xml.php) returns the same field names as QRZ.com so
this library mirrors the QRZ.com integration almost identically — but
uses a single long-lived Personal Access Token (PAT) instead of a
callsign/password → session-key dance.

Users generate a PAT at https://qrzcall.eu/ → My Profile → Account →
API Tokens. In Cloudlog they select "QRZCALL.EU" as their Callbook
Provider in Account Settings and paste the PAT into the Callbook
Password field (stored encrypted, per user, like the QRZ password);
the Callbook Username field is left blank. The library sends the PAT
as "Authorization: Bearer pat_…" on every lookup. Tokens are revocable
per-logger, so a compromised Cloudlog install can be locked out
without disturbing other clients.

Access tier: PAT generation requires a Data or Extra subscription on
QRZCALL.EU. The library doesn't need to know about subscription state —
the upstream endpoint enforces it and returns 401/403 if absent.

Sign-up + token UI: https://qrzcall.eu/
*/

class Qrzcall {

public $callbookname = 'QRZCALL';

// QRZCALL.EU's QRZ-compatible premium XML endpoint.
const XML_URL = 'https://api.qrzcall.eu/v1/pub/callsign_xml.php';

public function __construct()
{
$this->CI =& get_instance();
}

/**
* Look up a callsign with a Personal Access Token.
*
* @param string $callsign Callsign to query.
* @param string $token The user's "pat_…" Personal Access Token.
* @param bool $use_fullname True ⇒ "Firstname Lastname"; false ⇒ "Firstname" only.
* @return array Same shape as Qrz::search() — downstream Cloudlog code (QSO entry
* form, etc.) needs no changes.
*/
public function search($callsign, $token, $use_fullname = false) {
$data = null;
try {
$url = self::XML_URL . '?callsign=' . urlencode($callsign);

$ua = 'Cloudlog/' . $this->CI->config->item('app_version');

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $token]);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
$body = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpcode == 401) {
log_message('debug', 'QRZCALL.EU search 401 for ' . $callsign . ' — invalid or revoked token');
$data['error'] = 'Invalid or revoked QRZCALL.EU API token';
return $data;
}

if ($httpcode == 403) {
$data['error'] = 'QRZCALL.EU subscription required (Data or Extra tier)';
return $data;
}

if ($httpcode == 404) {
$data['error'] = 'Callsign not found';
return $data;
}

if ($httpcode != 200) {
log_message('debug', 'QRZCALL.EU search for ' . $callsign . ' returned HTTP ' . $httpcode);
$data['error'] = 'Problems with qrzcall.eu communication';
return $data;
}

$xml = simplexml_load_string($body);
if (!$xml || !isset($xml->Callsign)) {
$data['error'] = isset($xml->Error) ? (string)$xml->Error : 'Empty response';
return $data;
}

// QRZCALL.EU's XML uses identical field names to QRZ.com — we mirror the
// Qrz::search() field mapping exactly so downstream code needs no changes.
$data['callsign'] = (string)$xml->Callsign->call;
$data['name'] = (string)$xml->Callsign->fname;
$data['name_last'] = (string)$xml->Callsign->name;

if ($use_fullname === true) {
$data['name'] = trim($data['name'] . ' ' . $data['name_last']);
} else {
$data['name'] = trim($data['name']);
}

// Trim grid to 8 chars (max useful precision) — same rule as QRZ provider
$grid = (string)$xml->Callsign->grid;
$data['gridsquare'] = strlen($grid) > 8 ? substr($grid, 0, 8) : $grid;

$data['city'] = (string)$xml->Callsign->addr2; // backward-compat alias
$data['addr1'] = (string)$xml->Callsign->addr1;
$data['addr2'] = (string)$xml->Callsign->addr2;
$data['zip'] = (string)$xml->Callsign->zip;
$data['lat'] = (string)$xml->Callsign->lat;
$data['long'] = (string)$xml->Callsign->lon; // backward-compat alias
$data['lon'] = (string)$xml->Callsign->lon;
$data['country'] = (string)$xml->Callsign->country;
$data['dxcc'] = (string)$xml->Callsign->dxcc;
$data['iota'] = (string)$xml->Callsign->iota;
$data['qslmgr'] = (string)$xml->Callsign->qslmgr;
$data['image'] = (string)$xml->Callsign->image;
$data['email'] = (string)$xml->Callsign->email;
$data['lotw'] = (string)$xml->Callsign->lotw;
$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['ituz'] = $data['ituzone']; // backward-compat alias
$data['cqz'] = $data['cqzone']; // backward-compat alias
$data['xref'] = (string)$xml->Callsign->xref;
$data['aliases'] = (string)$xml->Callsign->aliases;
$data['ccode'] = (string)$xml->Callsign->ccode;
$data['state'] = (string)$xml->Callsign->state;
$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['url'] = (string)$xml->Callsign->url;
$data['bio'] = (string)$xml->Callsign->bio;
$data['biodate'] = (string)$xml->Callsign->biodate;
$data['imageinfo'] = (string)$xml->Callsign->imageinfo;
$data['moddate'] = (string)$xml->Callsign->moddate;
$data['geoloc'] = (string)$xml->Callsign->geoloc;
$data['born'] = (string)$xml->Callsign->born;
$data['nickname'] = (string)$xml->Callsign->nickname;

$data['us_county'] = ($data['country'] === 'United States') ? $data['county'] : null;

} finally {
return $data;
}
}

public function sourcename() {
return $this->callbookname;
}

}
24 changes: 24 additions & 0 deletions application/models/Logbook_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -5291,6 +5291,15 @@ public function check_missing_grid_id($all)
$callbook = $this->hamqth->search($callsign, $this->session->userdata('hamqth_session_key'));
}
}

if ($this->session->userdata('callbook_type') == "QRZCALL") {
// Lookup using QRZCALL.EU — stateless PAT auth, no session-key dance
$this->load->library('qrzcall');
$this->load->library('encryption');

$token = $this->encryption->decrypt($this->session->userdata('callbook_password'));
$callbook = $this->qrzcall->search($callsign, $token);
}
if (isset($callbook)) {
if (isset($callbook['error'])) {
printf("Error: " . $callbook['error'] . "<br />");
Expand Down Expand Up @@ -5446,6 +5455,21 @@ public function loadCallBook($callsign, $use_fullname = false)
$callbook = $this->hamqth->search($callsign, $this->session->userdata('hamqth_session_key'));
}
}

if ($this->session->userdata('callbook_type') == "QRZCALL") {
// Lookup using QRZCALL.EU — stateless PAT auth, no session-key dance needed.
// The user pastes their Personal Access Token into the "Callbook Password"
// field in Account Settings; it is stored encrypted like the QRZ password.
$this->load->library('qrzcall');
$this->load->library('encryption');
$token = $this->encryption->decrypt($this->session->userdata('callbook_password'));
$callbook = $this->qrzcall->search($callsign, $token, $use_fullname);

// If the base callsign lookup failed and it's a compound callsign, retry with base call
if (($callbook['callsign'] ?? '') == '' && strpos($callsign, '/') !== false) {
$callbook = $this->qrzcall->search($this->get_plaincall($callsign), $token, $use_fullname);
}
}
} finally {
return $callbook;
}
Expand Down
2 changes: 2 additions & 0 deletions application/views/user/edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -1148,10 +1148,12 @@
<option value="None" <?php if (isset($user_callbook_type) && $user_callbook_type == 'None') echo 'selected'; ?>>None</option>
<option value="QRZ" <?php if (isset($user_callbook_type) && $user_callbook_type == 'QRZ') echo 'selected'; ?>>QRZ</option>
<option value="HamQTH" <?php if (isset($user_callbook_type) && $user_callbook_type == 'HamQTH') echo 'selected'; ?>>HamQTH</option>
<option value="QRZCALL" <?php if (isset($user_callbook_type) && $user_callbook_type == 'QRZCALL') echo 'selected'; ?>>QRZCALL.EU</option>
</select>
<?php if (isset($callbook_type_error)) {
echo "<small class=\"error\">" . $callbook_type_error . "</small>";
} ?>
<small class="form-text text-muted">QRZCALL.EU: paste your Personal Access Token into the Callbook Password field below and leave Callbook Username blank.</small>
</div>

<div class="mb-3">
Expand Down