config->load('config'); } /* * Upload QSO to QRZ.com * When called from the url cloudlog/qrz/upload, the function loops through all station_id's with a qrz api key defined. * All QSOs not previously uploaded, will then be uploaded, one at a time */ public function upload() { $this->setOptions(); $this->load->model('logbook_model'); $station_ids = $this->logbook_model->get_station_id_with_qrz_api(); if ($station_ids) { foreach ($station_ids as $station) { $qrz_api_key = $station->qrzapikey; if($this->mass_upload_qsos($station->station_id, $qrz_api_key, true)) { echo "QSOs have been uploaded to QRZ.com."; log_message('info', 'QSOs have been uploaded to QRZ.com.'); } else{ echo "No QSOs found for upload."; log_message('info', 'No QSOs found for upload.'); } } } else { echo "No station profiles with a QRZ API Key found."; log_message('error', "No station profiles with a QRZ API Key found."); } } function setOptions() { $this->config->load('config'); ini_set('memory_limit', '-1'); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); } /* * Function gets all QSOs from given station_id, that are not previously uploaded to qrz. * Adif is build for each qso, and then uploaded, one at a time */ function mass_upload_qsos($station_id, $qrz_api_key, $trusted = false) { $i = 0; $data['qsos'] = $this->logbook_model->get_qrz_qsos($station_id, $trusted); $errormessages=array(); $this->load->library('AdifHelper'); if ($data['qsos']) { foreach ($data['qsos']->result() as $qso) { $adif = $this->adifhelper->getAdifLine($qso); if ($qso->COL_QRZCOM_QSO_UPLOAD_STATUS == 'M') { $result = $this->logbook_model->push_qso_to_qrz($qrz_api_key, $adif, true); } else { $result = $this->logbook_model->push_qso_to_qrz($qrz_api_key, $adif); } if ( ($result['status'] == 'OK') || ( ($result['status'] == 'error') && ($result['message'] == 'STATUS=FAIL&REASON=Unable to add QSO to database: duplicate&EXTENDED=')) ){ $this->markqso($qso->COL_PRIMARY_KEY); $i++; $result['status'] = 'OK'; } elseif ( ($result['status']=='error') && (substr($result['message'],0,11) == 'STATUS=AUTH')) { log_message('error', 'QRZ upload failed for qso: Call: ' . $qso->COL_CALL . ' Band: ' . $qso->COL_BAND . ' Mode: ' . $qso->COL_MODE . ' Time: ' . $qso->COL_TIME_ON); log_message('error', 'QRZ upload failed with the following message: ' .$result['message']); log_message('error', 'QRZ upload stopped for Station_ID: ' .$station_id); $errormessages[] = $result['message'] . ' Call: ' . $qso->COL_CALL . ' Band: ' . $qso->COL_BAND . ' Mode: ' . $qso->COL_MODE . ' Time: ' . $qso->COL_TIME_ON; $result['status'] = 'Error'; break; /* If key is invalid, immediate stop syncing for more QSOs of this station */ } else { log_message('error', 'QRZ upload failed for qso: Call: ' . $qso->COL_CALL . ' Band: ' . $qso->COL_BAND . ' Mode: ' . $qso->COL_MODE . ' Time: ' . $qso->COL_TIME_ON); log_message('error', 'QRZ upload failed with the following message: ' .$result['message']); $errormessages[] = $result['message'] . ' Call: ' . $qso->COL_CALL . ' Band: ' . $qso->COL_BAND . ' Mode: ' . $qso->COL_MODE . ' Time: ' . $qso->COL_TIME_ON; $result['status'] = 'Error'; } } if ($i == 0) { $result['status']='OK'; } $result['count'] = $i; $result['errormessages'] = $errormessages; return $result; } else { $result['status'] = 'Error'; $result['count'] = $i; $result['errormessages'] = $errormessages; return $result; } } /* * Function marks QSO with given primarykey as uploaded to qrz */ function markqso($primarykey) { $this->logbook_model->mark_qrz_qsos_sent($primarykey); } /* * Used for displaying the uid for manually selecting log for upload to qrz */ public function export() { $this->load->model('stations'); $data['page_title'] = "QRZ Logbook"; $data['station_profiles'] = $this->stations->all_of_user(); $data['station_profile'] = $this->stations->stations_with_qrz_api_key(); $this->load->model('Stations'); $data['callsigns'] = $this->Stations->callsigns_of_user($this->session->userdata('user_id')); $this->load->view('interface_assets/header', $data); $this->load->view('qrz/export'); $this->load->view('interface_assets/footer'); } /* * Used for ajax-function when selecting log for upload to qrz */ public function upload_station() { $this->setOptions(); $this->load->model('stations'); $postData = $this->input->post(); $this->load->model('logbook_model'); $result = $this->logbook_model->exists_qrz_api_key($postData['station_id']); $qrz_api_key = $result->qrzapikey; header('Content-type: application/json'); $result = $this->mass_upload_qsos($postData['station_id'], $qrz_api_key); if ($result['status'] == 'OK') { $stationinfo = $this->stations->stations_with_qrz_api_key(); $info = $stationinfo->result(); $data['status'] = 'OK'; $data['info'] = $info; $data['infomessage'] = $result['count'] . " QSOs are now uploaded to QRZ.com"; $data['errormessages'] = $result['errormessages']; echo json_encode($data); } else { $data['status'] = 'Error'; $data['info'] = 'Error: No QSOs found to upload.'; $data['errormessages'] = $result['errormessages']; echo json_encode($data); } } public function mark_qrz() { // Set memory limit to unlimited to allow heavy usage ini_set('memory_limit', '-1'); $station_id = $this->security->xss_clean($this->input->post('station_profile')); $this->load->model('adif_data'); $data['qsos'] = $this->adif_data->export_custom($this->input->post('from'), $this->input->post('to'), $station_id); $this->load->model('logbook_model'); foreach ($data['qsos']->result() as $qso) { $this->logbook_model->mark_qrz_qsos_sent($qso->COL_PRIMARY_KEY); } $this->load->view('interface_assets/header', $data); $this->load->view('qrz/mark_qrz', $data); $this->load->view('interface_assets/footer'); } public function import_qrz() { $this->load->model('user_model'); if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); } $data['page_title'] = "QRZ QSL Import"; $this->load->model('logbook_model'); $customDate = $this->input->post('from'); if ($customDate != NULL) { $qrz_last_date = date($customDate); } else { // Query the logbook to determine when the last LoTW confirmation was $qrz_last_date = null; } $this->download($this->session->userdata('user_id'),true); } // end function function download($user_id_to_load = null, $show_views = false) { // Remove $lastqrz parameter $this->load->model('user_model'); $this->load->model('logbook_model'); $api_keys = $this->logbook_model->get_qrz_apikeys(); $total_processed_count = 0; // Initialize total count here $data = []; // Initialize data array if ($api_keys) { foreach ($api_keys as $station) { if ((($user_id_to_load != null) && ($user_id_to_load != $station->user_id))) { // Skip User if we're called with a specific user_id continue; } // Remove the block checking for $lastqrz == null and fetching the date $qrz_api_key = $station->qrzapikey; $result = $this->mass_download_qsos($qrz_api_key); // mass_download_qsos returns ['table_data' => ..., 'processed_count' => ...] or ['status' => 'error', 'message' => ...] if ($result !== false && isset($result['processed_count'])) { $total_processed_count += $result['processed_count']; // Accumulate count $table_data = $result['table_data']; if (isset($table_data['tableheaders'])) { // Ensure headers are set only once if (!isset($data['tableheaders'])) { $data['tableheaders'] = $table_data['tableheaders']; } if (isset($table_data['table']) && $table_data['table'] != '') { if (isset($data['table'])) { $data['table'] .= $table_data['table']; } else { $data['table'] = $table_data['table']; } } } } else if (is_array($result) && isset($result['status']) && $result['status'] === 'error') { // Handle specific error structure returned by mass_download_qsos log_message('error', "Error during QRZ download for user_id: " . $station->user_id . ". Message: " . $result['message']); // Optionally echo error to user if $show_views is true, or add to $data['error'] if ($show_views) { $data['errors'][] = "Error for user ID " . $station->user_id . ": " . $result['message']; } } else { // Catch-all for unexpected return values (like the old boolean false or other issues) log_message('error', "Unexpected error or empty result returned from mass_download_qsos for API key associated with user_id: " . $station->user_id); if ($show_views) { $data['errors'][] = "Unexpected error during download for user ID " . $station->user_id . ". Check system logs."; } } } } else { echo "No station profiles with a QRZ API Key found."; log_message('error', "No station profiles with a QRZ API Key found."); // If no keys, we can exit early if showing views, or just let it fall through if not. if ($show_views) { $data['page_title'] = "QRZ ADIF Information"; $data['error'] = "No station profiles with a QRZ API Key found."; $this->load->view('interface_assets/header', $data); $this->load->view('qrz/analysis', $data); // Assuming view can show $error $this->load->view('interface_assets/footer'); return; // Stop further processing } else { return ''; // Return empty if not showing views and no keys found } } $this->load->model('user_model'); if ($this->user_model->authorize(2)) { // Only Output results if authorized User // Pass potential errors to the view if (isset($data['errors'])) { $view_data['errors'] = $data['errors']; } $has_matches_to_display = (isset($data['tableheaders']) && isset($data['table']) && $data['table'] != ''); $message = "Downloaded and processed " . $total_processed_count . " QSOs from QRZ."; if ($has_matches_to_display) { $message .= " Matching QSOs found and updated."; if ($show_views == TRUE) { $view_data['tableheaders'] = $data['tableheaders']; $view_data['table'] = $data['table'] . ''; $view_data['page_title'] = "QRZ ADIF Information"; $this->load->view('interface_assets/header', $view_data); $this->load->view('qrz/analysis', $view_data); // Pass $view_data containing table headers, rows, and errors $this->load->view('interface_assets/footer'); } else { echo $message; // Echo message when not showing views but matches were found // Optionally echo errors if any occurred if (isset($data['errors'])) { echo " Errors encountered: " . implode("; ", $data['errors']); } return ''; } } else { // No matches found in the logbook $message .= " No matching QSOs found in your logbook to update."; if ($show_views == TRUE) { $view_data['page_title'] = "QRZ ADIF Information"; $view_data['info_message'] = $message; // Pass the info message to the view // Errors are already in $view_data if they exist $this->load->view('interface_assets/header', $view_data); $this->load->view('qrz/analysis', $view_data); // Load view, assuming it checks for $info_message and $errors $this->load->view('interface_assets/footer'); } else { echo $message; // Echo message when not showing views and no matches found // Optionally echo errors if any occurred if (isset($data['errors'])) { echo " Errors encountered: " . implode("; ", $data['errors']); } return ''; } } } // End authorize check } function mass_download_qsos($qrz_api_key = '', $trusted = false) { // Remove $lastqrz parameter $config['upload_path'] = './uploads/'; $file = $config['upload_path'] . 'qrzcom_download_report.adi'; if (file_exists($file) && ! is_writable($file)) { // This part is fine - checks local file writability $error_message = "Temporary download file ".$file." is not writable. Aborting!"; // Return the structured error array here too for consistency return ['status' => 'error', 'message' => $error_message]; } $url = 'https://logbook.qrz.com/api'; // Correct URL $post_data['KEY'] = $qrz_api_key; // Correct parameter $post_data['ACTION'] = 'FETCH'; // Correct parameter $post_data['OPTION'] = 'TYPE:ADIF'; // Correct parameter for fetching all confirmed in ADIF $ch = curl_init( $url ); curl_setopt( $ch, CURLOPT_POST, true); // Correct method curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_data); // Correct data curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1); // Okay curl_setopt( $ch, CURLOPT_HEADER, 0); // Correct - don't need response headers curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); // Correct - get response as string curl_setopt( $ch, CURLOPT_TIMEOUT, 300); // 5 minute timeout curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 30); // 30 second connection timeout curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); curl_setopt($ch, CURLOPT_BUFFERSIZE, 128000); curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate'); $content = curl_exec($ch); // Get raw content $curl_error = curl_error($ch); // Check for cURL errors $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Get HTTP response code curl_close($ch); if ($curl_error) { // Check for cURL level errors first $error_message = "QRZ download cURL error: " . $curl_error; log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); return ['status' => 'error', 'message' => $error_message]; } if ($http_code !== 200) { $error_message = "QRZ download HTTP error: HTTP " . $http_code; log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); return ['status' => 'error', 'message' => $error_message]; } if ($content === false || $content === '') { // Check if curl_exec failed or returned empty $error_message = "QRZ download failed: No content received from QRZ.com."; log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); return ['status' => 'error', 'message' => $error_message]; } // Find the start of the ADIF data after "ADIF=" $adif_start_pos = strpos($content, 'ADIF='); if ($adif_start_pos !== false) { // Extract the content starting after "ADIF=" $content = substr($content, $adif_start_pos + 5); } else { // If "ADIF=" is not found, check for potential errors before assuming it's just ADIF if (strpos($content, 'STATUS=FAIL') !== false || strpos($content, 'STATUS=AUTH') !== false) { // Handle API errors even if ADIF= is missing $reason = $content; if (preg_match('/REASON=([^&]+)/', $content, $matches)) { $reason = urldecode($matches[1]); // Decode URL encoded reason } $error_message = "QRZ API Error: " . $reason; log_message('error', $error_message . ' API Key used: ' . $qrz_api_key . ' Raw Response: ' . $content); return ['status' => 'error', 'message' => $error_message]; } // If no error status and no ADIF=, maybe it's just ADIF? Or an unknown error. // Log a warning if content seems unusual but doesn't match known error patterns. if (trim($content) === '' || strlen(trim($content)) < 10) { // Arbitrary small length check log_message('error', 'QRZ download: Received unexpected content without ADIF= prefix or known error status. Content: ' . $content); // Decide if this should be treated as an error or empty ADIF // For now, let's treat it as potentially empty/invalid ADIF and let loadFromFile handle it. } } // Also remove the trailing metadata like &RESULT=OK&COUNT=... or just &COUNT=... $result_pos = strpos($content, '&RESULT='); $count_pos = strpos($content, '&COUNT='); $truncate_pos = false; if ($result_pos !== false && $count_pos !== false) { // Both found, take the earlier one $truncate_pos = min($result_pos, $count_pos); } elseif ($result_pos !== false) { // Only RESULT found $truncate_pos = $result_pos; } elseif ($count_pos !== false) { // Only COUNT found $truncate_pos = $count_pos; } if ($truncate_pos !== false) { $content = substr($content, 0, $truncate_pos); } // Check for QRZ API specific error messages if (strpos($content, 'STATUS=FAIL') !== false || strpos($content, 'STATUS=AUTH') !== false) { // Extract reason if possible, otherwise use full content $reason = $content; if (preg_match('/REASON=([^&]+)/', $content, $matches)) { $reason = urldecode($matches[1]); // Decode URL encoded reason } $error_message = "QRZ API Error: " . $reason; log_message('error', $error_message . ' API Key used: ' . $qrz_api_key . ' Raw Response: ' . $content); return ['status' => 'error', 'message' => $error_message]; } $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Save the potentially valid content if (file_put_contents($file, $content) === false) { $error_message = "Failed to write downloaded QRZ data to temporary file: " . $file; log_message('error', $error_message); return ['status' => 'error', 'message' => $error_message]; } else { // echo "Downloaded QRZ data to temporary file: " . $file; } // Proceed to load from the file ini_set('memory_limit', '-1'); $result = $this->loadFromFile($file); // loadFromFile returns ['table_data' => ..., 'processed_count' => ...] return $result; } /* |-------------------------------------------------------------------------- | Function: loadFromFile |-------------------------------------------------------------------------- | | $filepath is the ADIF file | | Internal function that takes the QRZ ADIF and imports into the log | */ private function loadFromFile($filepath) { // Figure out how we should be marking QSLs confirmed via LoTW $config['qrz_rcvd_mark'] = 'Y'; ini_set('memory_limit', '-1'); set_time_limit(1800); // 30 minutes max execution time instead of unlimited $this->load->library('adif_parser'); // Load the data from the file into the parser object $this->adif_parser->load_from_file($filepath); // <-- ADD THIS LINE // Now initialize the parser with the loaded data if (!$this->adif_parser->initialize()) { // Check return value of initialize // Handle initialization error (e.g., log it, return error structure) log_message('error', 'ADIF Parser initialization failed for file: ' . $filepath); // Return an error structure consistent with mass_download_qsos return ['status' => 'error', 'message' => 'ADIF Parser initialization failed. Check logs.']; } $tableheaders = "
| Station Callsign | "; $tableheaders .= "QSO Date | "; $tableheaders .= "Call | "; $tableheaders .= "Mode | "; $tableheaders .= "QRZ QSL Received | "; $tableheaders .= "QRZ Confirmed | "; $tableheaders .= "