skyview/assets/static/js/modules/callsign-manager.js
Ole-Morten Duesund 4a0a993e81 fix: Sanitize all innerHTML dynamic values to prevent XSS
Add centralized escapeHtml() utility and apply it to every dynamic value
inserted via innerHTML/template literals across the frontend. Data from
VRS JSON sources and external CSV files (airline names, countries) flows
through the backend as arbitrary strings that could contain HTML. While
Go's json.Marshal escapes < > &, JavaScript's JSON.parse reverses those
escapes before the values reach innerHTML — enabling script injection.

Affected modules: aircraft-manager, ui-manager, callsign-manager,
map-manager, and the 3D radar labels in app.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:21:56 +01:00

167 lines
No EOL
5.7 KiB
JavaScript

// Callsign enrichment and display module
import { escapeHtml } from './html-utils.js';
export class CallsignManager {
constructor() {
this.callsignCache = new Map();
this.pendingRequests = new Map();
// Rate limiting to avoid overwhelming the API
this.lastRequestTime = 0;
this.requestInterval = 100; // Minimum 100ms between requests
}
/**
* Get enriched callsign information, using cache when available
* @param {string} callsign - The raw callsign to lookup
* @returns {Promise<Object>} - Enriched callsign data
*/
async getCallsignInfo(callsign) {
if (!callsign || callsign.trim() === '') {
return null;
}
const cleanCallsign = callsign.trim().toUpperCase();
// Check cache first
if (this.callsignCache.has(cleanCallsign)) {
return this.callsignCache.get(cleanCallsign);
}
// Check if we already have a pending request for this callsign
if (this.pendingRequests.has(cleanCallsign)) {
return this.pendingRequests.get(cleanCallsign);
}
// Rate limiting
const now = Date.now();
if (now - this.lastRequestTime < this.requestInterval) {
await new Promise(resolve => setTimeout(resolve, this.requestInterval));
}
// Create the API request
const requestPromise = this.fetchCallsignInfo(cleanCallsign);
this.pendingRequests.set(cleanCallsign, requestPromise);
try {
const result = await requestPromise;
// Cache the result for future use
if (result && result.callsign) {
this.callsignCache.set(cleanCallsign, result.callsign);
}
return result ? result.callsign : null;
} catch (error) {
console.warn(`Failed to lookup callsign ${cleanCallsign}:`, error);
return null;
} finally {
// Clean up pending request
this.pendingRequests.delete(cleanCallsign);
this.lastRequestTime = Date.now();
}
}
/**
* Fetch callsign information from the API
* @param {string} callsign - The callsign to lookup
* @returns {Promise<Object>} - API response
*/
async fetchCallsignInfo(callsign) {
const response = await fetch(`/api/callsign/${encodeURIComponent(callsign)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
/**
* Generate rich HTML display for a callsign
* @param {Object} callsignInfo - Enriched callsign data from API
* @param {string} originalCallsign - Original callsign if API data is null
* @returns {string} - HTML string for display
*/
generateCallsignDisplay(callsignInfo, originalCallsign = '') {
const esc = escapeHtml;
if (!callsignInfo || !callsignInfo.is_valid) {
// Fallback for invalid or missing callsign data
if (originalCallsign) {
return `<span class="callsign-display simple">${esc(originalCallsign)}</span>`;
}
return '<span class="callsign-display no-data">N/A</span>';
}
const parts = [];
// Airline code
if (callsignInfo.airline_code) {
parts.push(`<span class="airline-code">${esc(callsignInfo.airline_code)}</span>`);
}
// Flight number
if (callsignInfo.flight_number) {
parts.push(`<span class="flight-number">${esc(callsignInfo.flight_number)}</span>`);
}
// Airline name (if available)
let airlineInfo = '';
if (callsignInfo.airline_name) {
airlineInfo = `<span class="airline-name" title="${esc(callsignInfo.airline_name)}">
${esc(callsignInfo.airline_name)}
</span>`;
// Add country if available
if (callsignInfo.airline_country) {
airlineInfo += ` <span class="airline-country">(${esc(callsignInfo.airline_country)})</span>`;
}
}
return `
<span class="callsign-display enriched">
<span class="callsign-code">${parts.join(' ')}</span>
${airlineInfo ? `<span class="callsign-details">${airlineInfo}</span>` : ''}
</span>
`;
}
/**
* Generate compact callsign display for table view
* @param {Object} callsignInfo - Enriched callsign data
* @param {string} originalCallsign - Original callsign fallback
* @returns {string} - Compact HTML for table display
*/
generateCompactCallsignDisplay(callsignInfo, originalCallsign = '') {
const esc = escapeHtml;
if (!callsignInfo || !callsignInfo.is_valid) {
return esc(originalCallsign) || 'N/A';
}
// For tables, use the display_name or format airline + flight
if (callsignInfo.display_name) {
return `<span class="callsign-compact" title="${esc(callsignInfo.airline_name || '')}">${esc(callsignInfo.display_name)}</span>`;
}
return `<span class="callsign-compact">${esc(callsignInfo.airline_code)} ${esc(callsignInfo.flight_number)}</span>`;
}
/**
* Clear the callsign cache (useful for memory management)
*/
clearCache() {
this.callsignCache.clear();
console.debug('Callsign cache cleared');
}
/**
* Get cache statistics for debugging
* @returns {Object} - Cache size and pending requests
*/
getCacheStats() {
return {
cacheSize: this.callsignCache.size,
pendingRequests: this.pendingRequests.size
};
}
}