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.