Skip to content

Commit 047b651

Browse files
authored
2.8.1
2.8.1
2 parents 9d10ba3 + 05a6e66 commit 047b651

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1561
-533
lines changed

.github/workflows/accessibility-alt-text-bot.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

application/config/migration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
|
2323
*/
2424

25-
$config['migration_version'] = 235;
25+
$config['migration_version'] = 236;
2626

2727
/*
2828
|--------------------------------------------------------------------------

application/controllers/Backup.php

Lines changed: 233 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,244 @@ function __construct()
55
{
66
parent::__construct();
77
}
8+
9+
/* ===== User-level JSON+ZIP backup (Stations, Logbooks, QSOs) ===== */
10+
public function user_export() {
11+
$this->load->model('user_model');
12+
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
13+
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
14+
15+
ini_set('memory_limit', '512M');
16+
$this->load->model('Stations');
17+
$this->load->model('Logbook_model');
18+
$this->load->dbutil();
19+
20+
$tmp_json = tempnam(sys_get_temp_dir(), 'cloudlog_backup_') . '.json';
21+
$tmp_zip = tempnam(sys_get_temp_dir(), 'cloudlog_backup_') . '.zip';
22+
23+
try {
24+
$user_id = $this->session->userdata('user_id');
25+
if (!$user_id) { show_error('User not authenticated.', 401); return; }
26+
27+
while (ob_get_level()) { ob_end_clean(); }
28+
29+
$result = array();
30+
31+
// Stations for user
32+
$this->db->where('user_id', $user_id);
33+
$stations = $this->db->get('station_profile')->result_array();
34+
$result['stations'] = $stations;
35+
36+
// QSOs per station
37+
$station_qsos = array();
38+
foreach ($stations as $station) {
39+
if (!isset($station['station_id'])) continue;
40+
$station_id = $station['station_id'];
41+
$qso_chunk_size = 10000; $qso_offset = 0; $qsos = array();
42+
do {
43+
$this->db->where('station_id', $station_id);
44+
$this->db->limit($qso_chunk_size, $qso_offset);
45+
$chunk = $this->db->get($this->config->item('table_name'))->result_array();
46+
$qsos = array_merge($qsos, $chunk); $qso_offset += $qso_chunk_size;
47+
} while (count($chunk) === $qso_chunk_size);
48+
$station_qsos[$station_id] = $qsos;
49+
}
50+
$result['station_qsos'] = $station_qsos;
51+
52+
// Logbooks for user (include QSOs snapshot for backwards compatibility)
53+
$logbooks = array();
54+
$this->db->where('user_id', $user_id);
55+
foreach ($this->db->get('station_logbooks')->result_array() as $logbook) {
56+
$station_id = isset($logbook['station_id']) ? $logbook['station_id'] : null;
57+
$qso_chunk_size = 10000; $qso_offset = 0; $qsos = array();
58+
if ($station_id) {
59+
do {
60+
$this->db->where('station_id', $station_id);
61+
$this->db->limit($qso_chunk_size, $qso_offset);
62+
$chunk = $this->db->get($this->config->item('table_name'))->result_array();
63+
$qsos = array_merge($qsos, $chunk); $qso_offset += $qso_chunk_size;
64+
} while (count($chunk) === $qso_chunk_size);
65+
}
66+
$logbook['qsos'] = $qsos; $logbooks[] = $logbook;
67+
}
68+
$result['logbooks'] = $logbooks;
69+
70+
// Relationships if present
71+
$tables = $this->db->list_tables();
72+
if (in_array('station_logbooks_entity', $tables)) {
73+
$this->db->where('user_id', $user_id);
74+
$result['logbooks_entity'] = $this->db->get('station_logbooks_entity')->result_array();
75+
} else { $result['logbooks_entity'] = []; }
76+
77+
$result['schema_version'] = '1.0';
78+
$result['exported_at'] = date('c');
79+
80+
file_put_contents($tmp_json, json_encode($result, JSON_PRETTY_PRINT));
81+
$zip = new ZipArchive();
82+
if ($zip->open($tmp_zip, ZipArchive::CREATE) !== TRUE) { throw new Exception('Could not create ZIP file'); }
83+
$zip->addFile($tmp_json, 'cloudlog_backup.json');
84+
$zip->close();
85+
86+
header('Content-Type: application/zip');
87+
header('Content-Disposition: attachment; filename="cloudlog_backup_'.date('Ymd_His').'.zip"');
88+
header('Content-Length: ' . filesize($tmp_zip));
89+
header('Cache-Control: no-store, no-cache');
90+
readfile($tmp_zip);
91+
@unlink($tmp_json); @unlink($tmp_zip); exit;
92+
} catch (Exception $e) { log_message('error', 'User export error: '.$e->getMessage()); show_error('Export failed: '.$e->getMessage(), 500); }
93+
}
94+
95+
public function user_import() {
96+
$this->load->model('user_model');
97+
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
98+
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
99+
100+
ini_set('memory_limit', '512M');
101+
$this->load->model('Stations');
102+
$this->load->model('Logbook_model');
103+
$this->load->library('session');
104+
105+
if (!isset($_FILES['backup_file']) || $_FILES['backup_file']['error'] !== UPLOAD_ERR_OK) { $this->session->set_flashdata('notice', 'No file uploaded or upload error.'); redirect('backup'); return; }
106+
107+
$uploaded_file = $_FILES['backup_file']['tmp_name'];
108+
$temp_json = tempnam(sys_get_temp_dir(), 'cloudlog_import_') . '.json';
109+
110+
$finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $uploaded_file); finfo_close($finfo);
111+
if ($mime_type === 'application/zip') {
112+
$zip = new ZipArchive();
113+
if ($zip->open($uploaded_file) === TRUE) {
114+
$json_content = $zip->getFromName('cloudlog_backup.json'); $zip->close();
115+
if ($json_content === false) { $this->session->set_flashdata('notice', 'Invalid backup file: JSON not found in ZIP.'); redirect('backup'); return; }
116+
file_put_contents($temp_json, $json_content); unset($json_content);
117+
} else { $this->session->set_flashdata('notice', 'Could not open ZIP file.'); redirect('backup'); return; }
118+
} else { copy($uploaded_file, $temp_json); }
119+
120+
$json = file_get_contents($temp_json); $data = json_decode($json, true); unset($json);
121+
if (!$data || !isset($data['stations']) || !isset($data['logbooks'])) { @unlink($temp_json); $this->session->set_flashdata('notice', 'Invalid backup file.'); redirect('backup'); return; }
122+
123+
$station_qsos = isset($data['station_qsos']) ? $data['station_qsos'] : array();
124+
$import_preview = array(
125+
'stations' => array_map(function($station) use ($station_qsos) {
126+
$station_id = isset($station['station_id']) ? $station['station_id'] : null;
127+
$qso_count = ($station_id && isset($station_qsos[$station_id])) ? count($station_qsos[$station_id]) : 0;
128+
return array(
129+
'station_id' => $station_id,
130+
'station_callsign' => $station['station_callsign'],
131+
'station_profile_name' => $station['station_profile_name'],
132+
'qsos_count' => $qso_count,
133+
);
134+
}, $data['stations']),
135+
'logbooks' => array_map(function($logbook) {
136+
$qso_count = isset($logbook['qsos']) ? count($logbook['qsos']) : 0;
137+
return array(
138+
'logbook_id' => $logbook['logbook_id'],
139+
'logbook_name' => $logbook['logbook_name'],
140+
'station_id' => isset($logbook['station_id']) ? $logbook['station_id'] : null,
141+
'qsos_count' => $qso_count,
142+
);
143+
}, $data['logbooks']),
144+
);
145+
146+
$this->session->set_userdata('import_backup_file', $temp_json);
147+
// HTMX partial
148+
$this->load->view('backup/import_preview', $import_preview);
149+
}
150+
151+
public function user_do_import() {
152+
$this->load->model('user_model');
153+
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
154+
if(!$this->user_model->authorize(2)) { echo '<div class="alert alert-danger">Not authorized</div>'; return; }
155+
156+
ini_set('memory_limit', '512M');
157+
$this->load->model('Stations');
158+
$this->load->model('Logbook_model');
159+
$this->load->library('session');
160+
161+
$temp_file = $this->session->userdata('import_backup_file');
162+
if (!$temp_file || !file_exists($temp_file)) { echo '<div class="alert alert-danger">No import data found.</div>'; return; }
163+
$json = file_get_contents($temp_file); $data = json_decode($json, true); unset($json);
164+
if (!$data) { echo '<div class="alert alert-danger">Failed to parse backup data.</div>'; @unlink($temp_file); $this->session->unset_userdata('import_backup_file'); return; }
165+
166+
$import_stations = $this->input->post('import_stations');
167+
$import_logbooks = $this->input->post('import_logbooks');
168+
if (!is_array($import_stations)) $import_stations = array();
169+
if (!is_array($import_logbooks)) $import_logbooks = array();
170+
if (empty($import_stations) && empty($import_logbooks)) { echo '<div class="alert alert-warning">Please select at least one station or logbook to import.</div>'; return; }
171+
172+
$current_user_id = $this->session->userdata('user_id');
173+
if (!$current_user_id) { echo '<div class="alert alert-danger">User not authenticated.</div>'; return; }
174+
175+
$total_stations = count($import_stations); $total_logbooks = count($import_logbooks); $total_qsos = 0;
176+
foreach ($data['logbooks'] as $logbook) { if (in_array($logbook['logbook_id'], $import_logbooks) && isset($logbook['qsos'])) { $total_qsos += count($logbook['qsos']); } }
177+
if (isset($data['station_qsos'])) { foreach ($data['station_qsos'] as $sid => $qsosArr) { if (in_array($sid, $import_stations)) { $total_qsos += count($qsosArr); } } }
178+
179+
$imported = array('stations'=>0,'logbooks'=>0,'qsos'=>0,'conflicts'=>array(),'step'=>'','total_stations'=>$total_stations,'total_logbooks'=>$total_logbooks,'total_qsos'=>$total_qsos);
180+
$station_id_map = array();
181+
182+
// Stations
183+
$imported['step'] = 'Importing stations';
184+
foreach ($data['stations'] as $station) {
185+
if (!in_array($station['station_id'], $import_stations)) continue; $old_station_id = $station['station_id'];
186+
$this->db->where('station_callsign', $station['station_callsign']);
187+
$this->db->where('station_profile_name', $station['station_profile_name']);
188+
$this->db->where('user_id', $current_user_id);
189+
$conflict = $this->db->get('station_profile')->row_array();
190+
if ($conflict) { $imported['conflicts'][] = 'Station exists (reusing): '.$station['station_callsign'].' - '.$station['station_profile_name'].' (ID '.$conflict['station_id'].')'; $station_id_map[$old_station_id] = $conflict['station_id']; continue; }
191+
unset($station['station_id']); $station['user_id'] = $current_user_id; $station['station_active'] = 0; // never active on import
192+
$this->db->insert('station_profile', $station); $new_station_id = $this->db->insert_id(); if (!$new_station_id) { $imported['conflicts'][] = 'Failed to create station: '.$station['station_callsign'].' - '.$station['station_profile_name']; continue; }
193+
$station_id_map[$old_station_id] = $new_station_id; $imported['stations']++;
194+
}
195+
196+
// Logbooks
197+
$imported['step'] = 'Importing logbooks';
198+
foreach ($data['logbooks'] as $logbook) {
199+
if (!in_array($logbook['logbook_id'], $import_logbooks)) continue;
200+
$this->db->where('logbook_name', $logbook['logbook_name']); $this->db->where('user_id', $current_user_id); $conflict = $this->db->get('station_logbooks')->row_array();
201+
if ($conflict) { $imported['conflicts'][] = 'Logbook exists (skipping): '.$logbook['logbook_name']; continue; }
202+
$logbook_copy = $logbook; unset($logbook_copy['qsos']); unset($logbook_copy['logbook_id']); if (isset($logbook_copy['active_logbook'])) unset($logbook_copy['active_logbook']); $logbook_copy['user_id'] = $current_user_id; if (isset($logbook_copy['station_id']) && isset($station_id_map[$logbook_copy['station_id']])) { $logbook_copy['station_id'] = $station_id_map[$logbook_copy['station_id']]; }
203+
$this->db->insert('station_logbooks', $logbook_copy); $imported['logbooks']++;
204+
}
205+
206+
// QSOs from station_qsos
207+
$imported['step'] = 'Importing QSOs';
208+
if (isset($data['station_qsos'])) {
209+
foreach ($data['station_qsos'] as $old_station_id => $qsos) {
210+
if (!in_array($old_station_id, $import_stations)) continue; if (!isset($station_id_map[$old_station_id])) continue; $new_station_id = $station_id_map[$old_station_id];
211+
foreach (array_chunk($qsos, 1000) as $qso_chunk) {
212+
foreach ($qso_chunk as $qso) {
213+
unset($qso['COL_PRIMARY_KEY']); $qso['station_id'] = $new_station_id;
214+
$this->db->where('COL_TIME_ON', $qso['COL_TIME_ON']); $this->db->where('COL_CALL', $qso['COL_CALL']); $this->db->where('station_id', $qso['station_id']);
215+
$conflict = $this->db->get($this->config->item('table_name'))->row_array(); if ($conflict) { $imported['conflicts'][] = 'QSO conflict: '.$qso['COL_CALL'].' @ '.$qso['COL_TIME_ON']; continue; }
216+
$this->db->insert($this->config->item('table_name'), $qso); $imported['qsos']++;
217+
}
218+
}
219+
}
220+
}
221+
// QSOs from logbooks (legacy)
222+
foreach ($data['logbooks'] as $logbook) {
223+
if (!in_array($logbook['logbook_id'], $import_logbooks)) continue; if (!isset($logbook['qsos'])) continue;
224+
foreach (array_chunk($logbook['qsos'], 1000) as $qso_chunk) {
225+
foreach ($qso_chunk as $qso) {
226+
unset($qso['COL_PRIMARY_KEY']); if (isset($qso['station_id']) && isset($station_id_map[$qso['station_id']])) { $qso['station_id'] = $station_id_map[$qso['station_id']]; }
227+
$this->db->where('COL_TIME_ON', $qso['COL_TIME_ON']); $this->db->where('COL_CALL', $qso['COL_CALL']); $this->db->where('station_id', $qso['station_id']);
228+
$conflict = $this->db->get($this->config->item('table_name'))->row_array(); if ($conflict) { $imported['conflicts'][] = 'QSO conflict: '.$qso['COL_CALL'].' @ '.$qso['COL_TIME_ON']; continue; }
229+
$this->db->insert($this->config->item('table_name'), $qso); $imported['qsos']++;
230+
}
231+
}
232+
}
233+
234+
$temp_file = $this->session->userdata('import_backup_file'); if ($temp_file && file_exists($temp_file)) { @unlink($temp_file); }
235+
$this->session->unset_userdata('import_backup_file');
236+
237+
$this->load->view('backup/import_progress', $imported);
238+
}
8239

9240
/* User Facing Links to Backup URLs */
10241
public function index()
11242
{
12243
$this->load->model('user_model');
13-
if(!$this->user_model->authorize(99)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
244+
// Allow all authenticated operators (level 2+) instead of only admins
245+
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
14246

15247
$data['page_title'] = "Backup";
16248

application/controllers/Labels.php

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ function prepareLabel($qsos, $jscall = false, $offset = 1, $grid = false, $via =
163163
$this->load->model('labels_model');
164164
$label = $this->labels_model->getDefaultLabel();
165165

166+
// Define font path before creating PDF object (only if not already defined)
167+
// Note: The path should NOT include 'unifont/' as AddFont() adds that automatically
168+
if (!defined('FPDF_FONTPATH')) {
169+
define('FPDF_FONTPATH', './src/Label/font/');
170+
}
166171

167172
try {
168173
if ($label) {
@@ -212,21 +217,23 @@ function prepareLabel($qsos, $jscall = false, $offset = 1, $grid = false, $via =
212217
}
213218
}
214219
} catch (\Throwable $th) {
220+
// Log the actual error for debugging
221+
log_message('error', 'Label generation error: ' . $th->getMessage() . ' in ' . $th->getFile() . ' on line ' . $th->getLine());
215222
if ($jscall) {
216223
header('Content-Type: application/json');
217-
echo json_encode(array('message' => 'Something went wrong! The label could not be generated. Check label size and font size.'));
224+
echo json_encode(array('message' => 'Something went wrong! The label could not be generated. Error: ' . $th->getMessage()));
218225
return;
219226
} else {
220-
$this->session->set_flashdata('error', 'Something went wrong! The label could not be generated. Check label size and font size.');
227+
$this->session->set_flashdata('error', 'Something went wrong! The label could not be generated. Error: ' . $th->getMessage());
221228
redirect('labels');
222229
}
223230
}
224-
define('FPDF_FONTPATH', './src/Label/font/');
225231

226232
$pdf->AddPage($ptype->orientation);
227233

228234
if ($label->font == 'DejaVuSans') { // leave this here, for future Use
229235
$pdf->AddFont($label->font,'','DejaVuSansMono.ttf',true);
236+
$pdf->AddFont($label->font,'B','DejaVuSans-Bold.ttf',true); // Add bold variant
230237
$pdf->SetFont($label->font,'');
231238
} else {
232239
$pdf->AddFont($label->font);
@@ -354,28 +361,35 @@ function finalizeData($pdf, $current_callsign, &$preliminaryData, $qso_per_label
354361
function generateLabel($pdf, $current_callsign, $tableData,$numofqsos,$qso,$orientation,$grid=true, $via=false, $awards=false){
355362
$builder = new \AsciiTable\Builder();
356363
$builder->addRows($tableData);
357-
$text = "Confirming QSO".($numofqsos>1 ? 's' : '')." with ";
358-
$text .= $current_callsign;
359-
if (($via) && ($qso['via'] ?? '' != '')) {
360-
$text.=' via '.substr($qso['via'],0,8);
361-
}
362-
$text .= "\n";
363-
$text .= $builder->renderTable();
364+
365+
// Build the header text (before callsign)
366+
$header = "Confirming QSO".($numofqsos>1 ? 's' : '')." with ";
367+
368+
// Build the callsign (to be rendered in bold + larger)
369+
$callsign = $current_callsign;
370+
if (($via) && ($qso['via'] ?? '' != '')) {
371+
$callsign .= ' via '.substr($qso['via'],0,8);
372+
}
373+
374+
// Build the rest of the text (after callsign)
375+
$rest = $builder->renderTable();
364376
if($qso['sat'] != "") {
365377
if (($qso['sat_mode'] == '') && ($qso['sat_band_rx'] !== '')) {
366-
$text .= "\n".'Satellite: '.$qso['sat'].' Band RX: '.$qso['sat_band_rx'];
378+
$rest .= "\n".'Satellite: '.$qso['sat'].' Band RX: '.$qso['sat_band_rx'];
367379
} elseif (($qso['sat_mode'] == '') && ($qso['sat_band_rx'] == '')) {
368-
$text .= "\n".'Satellite: '.$qso['sat'];
380+
$rest .= "\n".'Satellite: '.$qso['sat'];
369381
} else {
370-
$text .= "\n".'Satellite: '.$qso['sat'].' Mode: '.$qso['sat_mode'];
382+
$rest .= "\n".'Satellite: '.$qso['sat'].' Mode: '.$qso['sat_mode'];
371383
}
372384
}
373-
$text.="\n";
374-
if ($grid) { $text .= "My call: ".$qso['mycall']." Grid: ".$qso['mygrid']."\n"; }
375-
if ($awards) { $text .= $qso['awards']."\n"; }
376-
$text .= "Thanks for the QSO".($numofqsos>1 ? 's' : '');
377-
$text .= " | ".($qso['qsl_recvd'] == 'Y' ? 'TNX' : 'PSE')." QSL";
378-
$pdf->Add_Label($text,$orientation);
385+
$rest.="\n";
386+
if ($grid) { $rest .= "My call: ".$qso['mycall']." Grid: ".$qso['mygrid']."\n"; }
387+
if ($awards) { $rest .= $qso['awards']."\n"; }
388+
$rest .= "Thanks for the QSO".($numofqsos>1 ? 's' : '');
389+
$rest .= " | ".($qso['qsl_recvd'] == 'Y' ? 'TNX' : 'PSE')." QSL";
390+
391+
// Use the new method that renders callsign in bold and larger
392+
$pdf->Add_Label_With_Bold_Callsign($header, $callsign, $rest, $orientation);
379393
}
380394

381395
// New End

0 commit comments

Comments
 (0)