diff --git a/application/controllers/Logbook.php b/application/controllers/Logbook.php
index 756285e22..76b7e9898 100644
--- a/application/controllers/Logbook.php
+++ b/application/controllers/Logbook.php
@@ -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 Account Settings and select either QRZ or HamQTH in the "Callbook" section.';
+ $callsign['error'] = 'Online callbook not configured. Go to Account Settings and select QRZ, HamQTH or QRZCALL.EU in the "Callbook" section.';
}
if (isset($callsign['callsign']['gridsquare'])) {
@@ -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'));
diff --git a/application/libraries/Qrzcall.php b/application/libraries/Qrzcall.php
new file mode 100755
index 000000000..dabf2fbb4
--- /dev/null
+++ b/application/libraries/Qrzcall.php
@@ -0,0 +1,164 @@
+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;
+ }
+
+}
diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php
index e9d894d76..d26390f94 100755
--- a/application/models/Logbook_model.php
+++ b/application/models/Logbook_model.php
@@ -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'] . "
");
@@ -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;
}
diff --git a/application/views/user/edit.php b/application/views/user/edit.php
index 02bf13505..acf696414 100644
--- a/application/views/user/edit.php
+++ b/application/views/user/edit.php
@@ -1148,10 +1148,12 @@
+
" . $callbook_type_error . "";
} ?>
+ QRZCALL.EU: paste your Personal Access Token into the Callbook Password field below and leave Callbook Username blank.