mirror of
https://github.com/wavelog/wavelog
synced 2026-08-13 18:41:16 -04:00
Replaces the callsign+password → JWT flow with a single long-lived
Personal Access Token (pat_*), matching the GitHub/Stripe pattern.
User flow now:
• Sign up on qrzcall.eu (Data or Extra subscription required)
• Account → API Tokens → "Generate new token", label it
(e.g. "Wavelog at home"), copy the pat_… string
• Paste into Wavelog's "QRZCALL.EU API Token" config field
Why this is better than the previous credentials flow:
• The user's QRZCALL.EU password never leaves the SPA
• Each Wavelog install gets its own revocable token
• A leaked Wavelog config exposes a single revocable token,
not the password the user might reuse elsewhere
• The library is simpler: no session cache, no retry-on-expiry,
no /portable-aware re-auth dance — just Bearer on every call
Code changes:
• application/libraries/Qrzcall.php - drops session() and
set_session(), keeps search() and sourcename(). ~30 LOC shorter.
• application/libraries/Callbook.php - QRZCALL_SESSION_DURATION
constant and qrzcall_session_cachekey property removed.
_qrzcall() collapses from ~40 to ~20 LOC.
• application/config/config.sample.php - qrzcall_username and
qrzcall_password replaced with a single qrzcall_token.
• install/config/config.php - same in the wizard's template.
• install/index.php - the wizard now shows a single "API Token"
field when QRZCALL.EU is selected (toggled via JS, no extra
HTML page state machine). callbook_combination() validates
the pat_ prefix when present.
• install/includes/core/core_class.php - refactored to support
both username/password-style providers (qrz/hamqth/qrzcq/qrzru)
and token-style providers (qrzcall) cleanly. Drops the
"all callbooks use the same shape" assumption.
End-to-end re-tested:
✓ Live PAT lookup PA4R via the new library HTTP 200
✓ Unknown callsign XX9XX9XX error: "Callsign not found"
✓ Tampered token error: "Invalid or revoked QRZCALL.EU API token"
✓ Subscription gate (Free user with revoked sub) surfaces upstream 401
✓ php -l clean on all 6 files
✓ All 5 patches dry-run apply cleanly against fresh master
Existing JWT flow on api.qrzcall.eu/v1/auth/login.php remains
available — this PR moves Wavelog to the recommended pattern but
doesn't break anyone who already wired up JWT.
185 lines
7.4 KiB
PHP
185 lines
7.4 KiB
PHP
<?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. They paste it into Wavelog's "QRZCALL.EU API Token" config
|
|
field. The library presents it as "Authorization: Bearer pat_…" on
|
|
every lookup. Tokens are revocable per-logger, so a compromised Wavelog
|
|
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';
|
|
|
|
/**
|
|
* 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.
|
|
* @param bool $reduced True ⇒ blank out location-dependent fields (used
|
|
* after a /portable retry where we can't trust QTH).
|
|
* @return array Same shape as Qrz::search() — downstream Wavelog code (QSO entry
|
|
* form, etc.) needs no changes.
|
|
*/
|
|
public function search($callsign, $token, $use_fullname = false, $reduced = false) {
|
|
$data = null;
|
|
$ci = & get_instance();
|
|
try {
|
|
$url = self::XML_URL . '?callsign=' . urlencode($callsign);
|
|
|
|
$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, 'Wavelog/' . $ci->optionslib->get_option('version'));
|
|
$body = curl_exec($ch);
|
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
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['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->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;
|
|
|
|
if ($use_fullname === true) {
|
|
$data['name'] = $data['fname'] . ' ' . $data['name_last'];
|
|
} else {
|
|
$data['name'] = $data['fname'];
|
|
}
|
|
$data['name'] = trim($data['name']);
|
|
|
|
// Trim grid to 8 chars (max useful precision) — same rule as QRZ provider
|
|
$grid = $data['grid'];
|
|
$data['gridsquare'] = strlen($grid) > 8 ? substr($grid, 0, 8) : $grid;
|
|
|
|
// Backward-compat aliases — same as Qrz provider
|
|
$data['city'] = $data['addr2'];
|
|
$data['long'] = $data['lon'];
|
|
$data['ituz'] = $data['ituzone'];
|
|
$data['cqz'] = $data['cqzone'];
|
|
|
|
$data['us_county'] = ($data['country'] === 'United States') ? $data['county'] : null;
|
|
|
|
if ($reduced === true) {
|
|
$data['gridsquare'] = '';
|
|
$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 {
|
|
$data['source'] = $this->sourcename();
|
|
return $data;
|
|
}
|
|
}
|
|
|
|
public function sourcename() {
|
|
return $this->callbookname;
|
|
}
|
|
|
|
}
|