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>
167 lines
No EOL
5.7 KiB
JavaScript
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
|
|
};
|
|
}
|
|
} |