Skip to content
Merged
2 changes: 1 addition & 1 deletion application/config/migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
|
*/

$config['migration_version'] = 239;
$config['migration_version'] = 240;

/*
|--------------------------------------------------------------------------
Expand Down
22 changes: 18 additions & 4 deletions application/controllers/Logbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,12 @@ function view($id)

$this->load->model('logbook_model');
$data['query'] = $this->logbook_model->get_qso($id);
// Guard against inaccessible or missing QSO to avoid calling result() on null
if (!$data['query'] || $data['query']->num_rows() === 0) {
$this->session->set_flashdata('notice', "You're not allowed to do that!");
redirect('dashboard');
return;
}
$data['dxccFlag'] = $this->dxccflag->get($data['query']->result()[0]->COL_DXCC);

if ($this->session->userdata('user_measurement_base') == NULL) {
Expand Down Expand Up @@ -851,7 +857,9 @@ function partial($id)
if (isset($callsign['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($callsign['callsign']['dxcc']);
$callsign['callsign']['dxcc_name'] = $entity['name'];
if (is_array($entity) && isset($entity['name'])) {
$callsign['callsign']['dxcc_name'] = $entity['name'];
}
}
} elseif ($this->session->userdata('callbook_type') == "HamQTH") {
// Load the HamQTH library
Expand Down Expand Up @@ -884,7 +892,9 @@ function partial($id)
if (isset($callsign['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($callsign['callsign']['dxcc']);
$callsign['callsign']['dxcc_name'] = $entity['name'];
if (is_array($entity) && isset($entity['name'])) {
$callsign['callsign']['dxcc_name'] = $entity['name'];
}
}
if (isset($callsign['callsign']['error'])) {
$callsign['error'] = $callsign['callsign']['error'];
Expand Down Expand Up @@ -968,7 +978,9 @@ function search_result($id = "", $id2 = "")
if (isset($data['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($data['callsign']['dxcc']);
$data['callsign']['dxcc_name'] = $entity['name'];
if (is_array($entity) && isset($entity['name'])) {
$data['callsign']['dxcc_name'] = $entity['name'];
}
}
if (isset($data['callsign']['gridsquare'])) {
$this->load->model('logbook_model');
Expand Down Expand Up @@ -1005,7 +1017,9 @@ function search_result($id = "", $id2 = "")
if (isset($data['callsign']['dxcc'])) {
$this->load->model('logbook_model');
$entity = $this->logbook_model->get_entity($data['callsign']['dxcc']);
$data['callsign']['dxcc_name'] = $entity['name'];
if (is_array($entity) && isset($entity['name'])) {
$data['callsign']['dxcc_name'] = $entity['name'];
}
}
if (isset($data['callsign']['error'])) {
$data['error'] = $data['callsign']['error'];
Expand Down
4 changes: 3 additions & 1 deletion application/controllers/Logbooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public function delete_relationship($logbook_id, $station_id) {
}

// Get station location details
$station = $this->stations->profile($station_id);
$station = $this->stations->profile_clean($station_id);

if ($station) {
$is_owner = $this->logbooks_model->is_logbook_owner($logbook_id);
Expand All @@ -144,6 +144,8 @@ public function delete_relationship($logbook_id, $station_id) {
} else {
$this->session->set_flashdata('notice', 'You can only unlink your own station locations');
}
} else {
$this->session->set_flashdata('notice', 'Station location not found');
}

redirect('logbooks/edit/'.$logbook_id);
Expand Down
7 changes: 6 additions & 1 deletion application/controllers/Qrz.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,15 @@ private function loadFromFile($filepath) {
}

$call = str_replace("_","/",$record['call']);
$station_callsign = str_replace("_","/",$record['station_callsign']);
$station_callsign = str_replace("_","/",$record['station_callsign'] ?? '');
$band = $record['band'] ?? ''; // Ensure band exists
$mode = $record['mode'] ?? ''; // Ensure mode exists

// Log if station_callsign is missing
if (!isset($record['station_callsign']) || $record['station_callsign'] === '') {
log_message('error', 'QRZ import: Missing station_callsign for QSO with callsign: ' . $call . ' on ' . $time_on);
}

// Add record data to batch
$batch_data[] = [
'time_on' => $time_on,
Expand Down
30 changes: 30 additions & 0 deletions application/migrations/240_tag_2_8_4.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

defined('BASEPATH') OR exit('No direct script access allowed');

/*
* Tag Cloudlog as 2.8.4 Migration
*/

class Migration_tag_2_8_4 extends CI_Migration {

public function up()
{

// Tag Cloudlog 2.8.4
$this->db->where('option_name', 'version');
$this->db->update('options', array('option_value' => '2.8.4'));

// Trigger Version Info Dialog
$this->db->where('option_type', 'version_dialog');
$this->db->where('option_name', 'confirmed');
$this->db->update('user_options', array('option_value' => 'false'));

}

public function down()
{
$this->db->where('option_name', 'version');
$this->db->update('options', array('option_value' => '2.8.3'));
}
}
30 changes: 27 additions & 3 deletions application/models/Logbook_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -5026,15 +5026,39 @@ function wab_qso_details($wab)

public function check_qso_is_accessible($id)
{
// check if qso belongs to user
// Allow access if QSO belongs to current user OR
// if QSO's station is part of the user's active logbook (including shared permissions)

// First: owner check (current behavior)
$this->db->select($this->config->item('table_name') . '.COL_PRIMARY_KEY');
$this->db->join('station_profile', $this->config->item('table_name') . '.station_id = station_profile.station_id');
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->where($this->config->item('table_name') . '.COL_PRIMARY_KEY', $id);
$query = $this->db->get($this->config->item('table_name'));
if ($query->num_rows() == 1) {
$ownerQuery = $this->db->get($this->config->item('table_name'));
if ($ownerQuery->num_rows() == 1) {
return true;
}

// Second: shared-logbook check via active logbook relationships
$CI = &get_instance();
$CI->load->model('logbooks_model');
$activeLogbookId = $this->session->userdata('active_station_logbook');

// Ensure user has at least read access to the active logbook
if ($activeLogbookId && $CI->logbooks_model->check_logbook_is_accessible($activeLogbookId, 'read')) {
$logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($activeLogbookId);

if (!empty($logbooks_locations_array)) {
$this->db->select($this->config->item('table_name') . '.COL_PRIMARY_KEY');
$this->db->where_in($this->config->item('table_name') . '.station_id', $logbooks_locations_array);
$this->db->where($this->config->item('table_name') . '.COL_PRIMARY_KEY', $id);
$sharedQuery = $this->db->get($this->config->item('table_name'));
if ($sharedQuery->num_rows() == 1) {
return true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read-only users can delete/modify QSOs in shared logbooks

The expanded check_qso_is_accessible function now allows access when a user has only 'read' permission to a shared logbook. However, this function is used as a gatekeeper for destructive operations like delete() and paperqsl_update(). This means a user granted read-only access to a shared logbook can now delete or modify QSOs they shouldn't be able to change. The shared-logbook check uses check_logbook_is_accessible($activeLogbookId, 'read') but the calling code in Qso.php uses the result to authorize deletions and updates.

Fix in Cursor Fix in Web

}
}

return false;
}

Expand Down
12 changes: 10 additions & 2 deletions application/models/Setup_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ function getCountryCount() {

function getLogbookCount() {
$userid = xss_clean($this->session->userdata('user_id'));
$sql = 'select count(*) as count from station_logbooks where user_id =' . $userid;
// Count logbooks the user owns OR has been granted permissions to
$sql = "SELECT COUNT(DISTINCT sl.logbook_id) AS count
FROM station_logbooks sl
LEFT JOIN station_logbooks_permissions slp
ON slp.logbook_id = sl.logbook_id AND slp.user_id = {$userid}
WHERE sl.user_id = {$userid} OR slp.user_id = {$userid}";
$query = $this->db->query($sql);

return $query->row()->count;
Expand All @@ -31,7 +36,10 @@ function getAllSetupCounts() {

$sql = "SELECT
(SELECT COUNT(*) FROM dxcc_entities) as country_count,
(SELECT COUNT(*) FROM station_logbooks WHERE user_id = {$userid}) as logbook_count,
(SELECT COUNT(DISTINCT sl.logbook_id) FROM station_logbooks sl
LEFT JOIN station_logbooks_permissions slp
ON slp.logbook_id = sl.logbook_id AND slp.user_id = {$userid}
WHERE sl.user_id = {$userid} OR slp.user_id = {$userid}) as logbook_count,
(SELECT COUNT(*) FROM station_profile WHERE user_id = {$userid}) as location_count";

$query = $this->db->query($sql);
Expand Down
74 changes: 74 additions & 0 deletions application/views/logbooks/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ class="btn btn-primary btn-sm"
title="<?php echo lang('station_logbooks_edit_logbook') . ': ' . $row->logbook_name;?>">
<i class="fas fa-edit"></i>
</a>
<?php if($row->public_slug != '') { ?>
<button class="btn btn-success btn-sm"
title="Get Embed Code"
data-bs-toggle="modal"
data-bs-target="#embedModal"
onclick="setEmbedCode('<?php echo $row->public_slug; ?>', '<?php echo addslashes($row->logbook_name); ?>')">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JavaScript escaping breaks embed modal for special characters

The addslashes() function is used to escape logbook_name for a JavaScript string in an onclick handler, but addslashes() only escapes quotes, backslashes, and NUL bytes. It does not escape newlines, carriage returns, or other control characters. If a logbook name contains any of these characters, the resulting JavaScript string literal would be syntactically invalid, causing the embed button to fail silently. Using json_encode() would properly escape all special characters for JavaScript context.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JavaScript escaping breaks embed modal for special characters

The addslashes() function is used to escape logbook_name for a JavaScript string in an onclick handler, but addslashes() only escapes quotes, backslashes, and NUL bytes. It does not escape newlines, carriage returns, or other control characters. If a logbook name contains any of these characters, the resulting JavaScript string literal would be syntactically invalid, causing the embed button to fail silently. Using json_encode() would properly escape all special characters for JavaScript context.

Fix in Cursor Fix in Web

<i class="fas fa-code"></i>
</button>
<?php } ?>
<?php if($row->user_id == $this->session->userdata('user_id') || (isset($row->access_level) && $row->access_level == 'admin')) { ?>
<a href="<?php echo site_url('logbooks/manage_sharing')."/".$row->logbook_id; ?>"
class="btn btn-info btn-sm"
Expand Down Expand Up @@ -167,3 +176,68 @@ class="btn btn-danger btn-sm"
<?php } ?>

</div>

<!-- Embed Code Modal -->
<div class="modal fade" id="embedModal" tabindex="-1" aria-labelledby="embedModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="embedModalLabel">
<i class="fas fa-code me-2"></i>Embed Widget - <span id="embedLogbookName"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted mb-3">Copy the code below and paste it into your website to embed your logbook widget:</p>
<div class="mb-3">
<label for="embedCodeInput" class="form-label">Embed Code</label>
<div class="input-group">
<textarea class="form-control" id="embedCodeInput" rows="4" readonly></textarea>
<button class="btn btn-primary" type="button" id="copyEmbedBtn" title="Copy to clipboard">
<i class="fas fa-copy me-1"></i>Copy
</button>
</div>
</div>
<div class="alert alert-info" role="alert">
<strong>Note:</strong> Make sure your logbook has a public slug configured before embedding.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

<script>
function setEmbedCode(publicSlug, logbookName) {
const baseUrl = '<?php echo site_url('widgets/qsos'); ?>';
const iframeCode = `<iframe src="${baseUrl}/${publicSlug}" width="100%" height="600" frameborder="0" allowfullscreen></iframe>`;

document.getElementById('embedCodeInput').value = iframeCode;
document.getElementById('embedLogbookName').textContent = logbookName;
}

document.addEventListener('DOMContentLoaded', function() {
const copyBtn = document.getElementById('copyEmbedBtn');
if (copyBtn) {
copyBtn.addEventListener('click', function() {
const textarea = document.getElementById('embedCodeInput');
textarea.select();
document.execCommand('copy');

// Visual feedback
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
copyBtn.classList.add('btn-success');
copyBtn.classList.remove('btn-primary');

setTimeout(() => {
copyBtn.innerHTML = originalText;
copyBtn.classList.remove('btn-success');
copyBtn.classList.add('btn-primary');
}, 2000);
});
}
});
</script>
3 changes: 3 additions & 0 deletions application/views/widgets/qsos.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
</tr>
<?php $i++; } ?>
</table>
<div style="text-align: right; margin-top: 10px; font-size: 0.85em; color: #999;">
Powered by <a href="https://github.com/magicbug/Cloudlog" target="_blank" style="color: #0066cc; text-decoration: none;">Cloudlog</a>
</div>
</body>

</html>