/** * Status Control - Document statistics and change tracking */ class StatusControl { constructor() { this.control = Object.create(Control); // Configure for status functionality this.control.config = { icon: '📊', title: 'Status', className: 'status-control', defaultContent: 'Document statistics and changes', ariaLabel: 'Status Control', position: 'e', // East positioning footer: `Updated ${new Date().toLocaleTimeString()}` }; // Initialize change tracking this.control.changeTracking = { headings: new Set(), sections: new Set(), images: new Set(), tables: new Set(), lastScanTime: null, initialCounts: { headings: 0, sections: 0, images: 0, tables: 0, lines: 0, words: 0, characters: 0 } }; this.bindMethods(); } bindMethods() { // Bind utility functions this.control.safeTextExtraction = this.safeTextExtraction.bind(this); this.control.sanitizeText = this.sanitizeText.bind(this); this.control.validateElement = this.validateElement.bind(this); this.control.safeStatsOperation = this.safeStatsOperation.bind(this); // Bind existing methods this.control.calculateStats = this.calculateStats.bind(this); this.control.isContentSection = this.isContentSection.bind(this); this.control.isContentTable = this.isContentTable.bind(this); this.control.updateChangeTracking = this.updateChangeTracking.bind(this); this.control.buildContent = this.buildContent.bind(this); this.control.refreshStats = this.refreshStats.bind(this); this.control.resetChangeTracking = this.resetChangeTracking.bind(this); this.control.setupAutoRefresh = this.setupAutoRefresh.bind(this); // Override collapse to clean up intervals const originalCollapse = this.control.collapse; this.control.collapse = () => { if (this.control.autoRefreshInterval) { clearInterval(this.control.autoRefreshInterval); this.control.autoRefreshInterval = null; } originalCollapse.call(this.control); }; } // Utility functions for safe operations safeTextExtraction(element) { if (!this.validateElement(element)) { return ''; } try { const text = element.textContent || element.innerText || ''; return this.sanitizeText(text.trim()); } catch (error) { console.warn('Text extraction failed:', error); return ''; } } sanitizeText(text) { if (typeof text !== 'string') { return ''; } // Remove potentially harmful characters and limit length const maxLength = 100000; // 100KB text limit const sanitized = text .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars .slice(0, maxLength); // Limit length return sanitized; } validateElement(element) { return element && element.nodeType === Node.ELEMENT_NODE && element.isConnected && !element.closest('.control-panel'); // Avoid control elements } safeStatsOperation(operation, fallback = 0, context = 'stats') { try { const result = operation(); // Validate numeric results return typeof result === 'number' && isFinite(result) ? result : fallback; } catch (error) { console.warn(`Stats operation failed in ${context}:`, error); if (window.MarkitectDebugSystem) { window.MarkitectDebugSystem.addMessage( `Stats operation failed: ${error.message}`, 'WARNING', 'StatusControl', { context, eventType: 'ERROR' } ); } return fallback; } } calculateStats() { const stats = { headings: { total: 0, changed: 0 }, sections: { total: 0, changed: 0 }, images: { total: 0, changed: 0 }, tables: { total: 0, changed: 0 }, document: { lines: 0, words: 0, characters: 0 }, sections_detail: { lines: 0, words: 0, characters: 0 }, tables_detail: { lines: 0, words: 0, characters: 0 } }; return this.safeStatsOperation(() => { // Count headings (h1-h6, excluding control titles) const headings = this.control.safeQuerySelectorAll('h1, h2, h3, h4, h5, h6'); const maxElements = 10000; // Limit processing to prevent DoS headings.slice(0, maxElements).forEach(heading => { if (!this.validateElement(heading)) return; const text = this.safeTextExtraction(heading).toLowerCase(); // Skip control headings with enhanced filtering const controlKeywords = ['contents', 'debug', 'status', 'control', 'menu', 'toolbar']; const isControlHeading = controlKeywords.some(keyword => text.includes(keyword)); if (text.length > 0 && !isControlHeading) { stats.headings.total++; const fullText = this.safeTextExtraction(heading); if (this.control.changeTracking.headings.has(fullText)) { stats.headings.changed++; } } }); // Count sections (content blocks excluding headings and table cells) const sections = this.control.safeQuerySelectorAll('p, blockquote, pre, li, div'); sections.slice(0, maxElements).forEach(section => { if (this.isContentSection(section)) { stats.sections.total++; const sectionText = this.safeTextExtraction(section); if (sectionText.length > 0) { const lines = this.safeStatsOperation(() => sectionText.split('\n').length, 0, 'countLines'); const words = this.safeStatsOperation(() => sectionText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countWords'); const characters = Math.min(sectionText.length, 1000000); // Cap at 1MB stats.sections_detail.lines += lines; stats.sections_detail.words += words; stats.sections_detail.characters += characters; if (this.control.changeTracking.sections.has(sectionText)) { stats.sections.changed++; } } } }); // Count tables as separate entities const tables = this.control.safeQuerySelectorAll('table'); tables.slice(0, maxElements).forEach(table => { if (this.isContentTable(table)) { stats.tables.total++; const tableText = this.safeTextExtraction(table); if (tableText.length > 0) { const lines = this.safeStatsOperation(() => tableText.split('\n').length, 0, 'countTableLines'); const words = this.safeStatsOperation(() => tableText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countTableWords'); const characters = Math.min(tableText.length, 1000000); // Cap at 1MB stats.tables_detail.lines += lines; stats.tables_detail.words += words; stats.tables_detail.characters += characters; // Generate safer table identifier const tableId = this.sanitizeText(table.id || table.outerHTML.substring(0, 100).replace(/[<>"'&]/g, '')); if (this.control.changeTracking.tables.has(tableId)) { stats.tables.changed++; } } } }); // Count images with validation const images = this.control.safeQuerySelectorAll('img'); images.slice(0, maxElements).forEach(img => { if (this.validateElement(img)) { stats.images.total++; // Safely extract and validate image source const imgSrc = this.sanitizeText(img.src || img.getAttribute('src') || ''); if (imgSrc && this.control.changeTracking.images.has(imgSrc)) { stats.images.changed++; } } }); // Calculate total document stats with protection const bodyText = this.safeTextExtraction(document.body); if (bodyText) { const cleanText = bodyText.replace(/\s+/g, ' '); stats.document.lines = this.safeStatsOperation(() => bodyText.split('\n').length, 0, 'countDocLines'); stats.document.words = this.safeStatsOperation(() => cleanText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countDocWords'); stats.document.characters = Math.min(cleanText.length, 10000000); // Cap at 10MB } return stats; }, stats, 'calculateStats'); } isContentSection(element) { return this.safeStatsOperation(() => { if (!this.validateElement(element)) { return false; } // Enhanced control detection with timeout protection let current = element; let depth = 0; const maxDepth = 50; // Prevent infinite loops while (current && current !== document.body && depth < maxDepth) { if (current.classList && ( current.classList.contains('control-panel') || current.classList.contains('control-content') || current.classList.contains('control-header') || current.className.includes('control') || current.id?.includes('control') )) { return false; } current = current.parentElement; depth++; } // Skip if element is inside a table (tables are counted separately) if (element.closest && element.closest('table')) { return false; } // Skip if element has no meaningful text content const text = this.safeTextExtraction(element); return text.length > 0 && text.length < 50000; // Reasonable size limit }, false, 'isContentSection'); } isContentTable(table) { return this.safeStatsOperation(() => { if (!this.validateElement(table) || table.tagName !== 'TABLE') { return false; } // Enhanced control detection with depth limiting let current = table; let depth = 0; const maxDepth = 50; while (current && current !== document.body && depth < maxDepth) { if (current.classList && ( current.classList.contains('control-panel') || current.classList.contains('control-content') || current.classList.contains('control-header') || current.className.includes('control') || current.id?.includes('control') )) { return false; } current = current.parentElement; depth++; } // Check if table has meaningful content with limits const text = this.safeTextExtraction(table); return text.length > 0 && text.length < 100000; // Reasonable table size limit }, false, 'isContentTable'); } updateChangeTracking() { const now = Date.now(); // Headings const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); headings.forEach(heading => { const text = heading.textContent.trim(); if (text && !text.toLowerCase().includes('control')) { const changed = heading.dataset.lastModified && (now - parseInt(heading.dataset.lastModified)) < 300000; // 5 minutes if (changed) { this.control.changeTracking.headings.add(text); } } }); // Sections const sections = document.querySelectorAll('p, blockquote, pre, li, div'); sections.forEach(section => { if (this.isContentSection(section)) { const text = section.textContent.trim(); if (text.length > 0) { const changed = section.dataset.lastModified && (now - parseInt(section.dataset.lastModified)) < 300000; // 5 minutes if (changed) { this.control.changeTracking.sections.add(text); } } } }); // Tables const tables = document.querySelectorAll('table'); tables.forEach(table => { if (this.isContentTable(table)) { const tableId = table.id || table.outerHTML.substring(0, 100); const changed = table.dataset.lastModified && (now - parseInt(table.dataset.lastModified)) < 300000; // 5 minutes if (changed) { this.control.changeTracking.tables.add(tableId); } } }); // Images const images = document.querySelectorAll('img'); images.forEach(img => { const src = img.src || img.getAttribute('src') || ''; const changed = img.dataset.lastModified && (now - parseInt(img.dataset.lastModified)) < 300000; // 5 minutes if (changed && src) { this.control.changeTracking.images.add(src); } }); this.control.changeTracking.lastScanTime = now; } buildContent() { this.control.safeOperation(() => { console.log("📊 Building status control content..."); const content = this.control.safeQuerySelector('.control-content', this.control.element); if (!content) { console.error("📊 Status control content element not found"); return; } // Update tracking and calculate stats with timeout protection const timeout = setTimeout(() => { console.warn('Status content build operation timed out'); }, 10000); // 10 second timeout this.updateChangeTracking(); const stats = this.calculateStats(); clearTimeout(timeout); // Sanitize numeric values to prevent injection const safeStats = { document: { lines: Math.max(0, Math.floor(stats.document.lines || 0)), words: Math.max(0, Math.floor(stats.document.words || 0)), characters: Math.max(0, Math.floor(stats.document.characters || 0)) }, headings: { total: Math.max(0, Math.floor(stats.headings.total || 0)), changed: Math.max(0, Math.floor(stats.headings.changed || 0)) }, sections: { total: Math.max(0, Math.floor(stats.sections.total || 0)), changed: Math.max(0, Math.floor(stats.sections.changed || 0)) }, sections_detail: { lines: Math.max(0, Math.floor(stats.sections_detail.lines || 0)), words: Math.max(0, Math.floor(stats.sections_detail.words || 0)), characters: Math.max(0, Math.floor(stats.sections_detail.characters || 0)) }, tables: { total: Math.max(0, Math.floor(stats.tables.total || 0)), changed: Math.max(0, Math.floor(stats.tables.changed || 0)) }, tables_detail: { lines: Math.max(0, Math.floor(stats.tables_detail.lines || 0)), words: Math.max(0, Math.floor(stats.tables_detail.words || 0)), characters: Math.max(0, Math.floor(stats.tables_detail.characters || 0)) }, images: { total: Math.max(0, Math.floor(stats.images.total || 0)), changed: Math.max(0, Math.floor(stats.images.changed || 0)) } }; // Use safe stats for display with proper escaping content.innerHTML = `
📄 Document
Lines: ${safeStats.document.lines.toLocaleString()} | Words: ${safeStats.document.words.toLocaleString()} | Chars: ${safeStats.document.characters.toLocaleString()}
📋 Headings: ${safeStats.headings.total} ${safeStats.headings.changed > 0 ? ` (+${safeStats.headings.changed})` : ''}
📄 Sections: ${safeStats.sections.total} ${safeStats.sections.changed > 0 ? ` (+${safeStats.sections.changed})` : ''}
Lines: ${safeStats.sections_detail.lines.toLocaleString()} | Words: ${safeStats.sections_detail.words.toLocaleString()} | Chars: ${safeStats.sections_detail.characters.toLocaleString()}
🗂️ Tables: ${safeStats.tables.total} ${safeStats.tables.changed > 0 ? ` (+${safeStats.tables.changed})` : ''}
Lines: ${safeStats.tables_detail.lines.toLocaleString()} | Words: ${safeStats.tables_detail.words.toLocaleString()} | Chars: ${safeStats.tables_detail.characters.toLocaleString()}
🖼️ Images: ${safeStats.images.total} ${safeStats.images.changed > 0 ? ` (+${safeStats.images.changed})` : ''}
`; // Add safer event listeners instead of inline onclick const refreshBtn = content.querySelector('#status-refresh-btn'); const resetBtn = content.querySelector('#status-reset-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.control.safeOperation(() => { if (window.statusControl && window.statusControl.refreshStats) { window.statusControl.refreshStats(); } }, null, 'refreshButton'); }); } if (resetBtn) { resetBtn.addEventListener('click', () => { this.control.safeOperation(() => { if (window.statusControl && window.statusControl.resetChangeTracking) { window.statusControl.resetChangeTracking(); } }, null, 'resetButton'); }); } console.log("📊 Status control content built successfully"); // Set up auto-refresh this.setupAutoRefresh(); // Show panel and expand this.control.expand(); }, () => { console.error("📊 Error in buildContent: Failed to build status control content"); const content = this.control.safeQuerySelector('.control-content', this.control.element); if (content) { content.innerHTML = '
Status loading failed
'; } }, 'buildContent'); } refreshStats() { if (this.control.isExpanded) { this.updateChangeTracking(); // Update footer timestamp this.control.config.footer = `Updated ${new Date().toLocaleTimeString()}`; this.control.styleFooter(); const content = this.control.element.querySelector('.control-content'); if (content) { const stats = this.calculateStats(); // Update the display without rebuilding entire content this.buildContent(); } } } resetChangeTracking() { if (confirm('Reset all document changes? This will revert all sections to their original state.')) { console.log('📊 Resetting document changes...'); // Reset using available infrastructure if (window.sectionManager && window.domRenderer) { // Use the proper document management infrastructure try { // Hide any open editors window.domRenderer.hideCurrentEditor(); // Reset all sections to original state const allSections = Array.from(window.sectionManager.sections.values()); allSections.forEach(section => { section.resetToOriginal(); }); // Re-render all sections window.domRenderer.renderAllSections(allSections); console.log('📊 Document reset successful'); // Add to debug system if (window.MarkitectDebugSystem) { window.MarkitectDebugSystem.addMessage( `Document reset completed - ${allSections.length} sections restored`, 'SUCCESS', 'StatusControl', { eventType: 'SYSTEM' } ); } } catch (error) { console.error('📊 Document reset failed:', error); if (window.MarkitectDebugSystem) { window.MarkitectDebugSystem.addMessage( `Document reset failed: ${error.message}`, 'ERROR', 'StatusControl', { eventType: 'SYSTEM' } ); } } } else { // Fallback to page reload if infrastructure not available console.log('📊 Document management infrastructure not available, using page reload'); window.location.reload(); } // Clear our own change tracking this.control.changeTracking.headings.clear(); this.control.changeTracking.sections.clear(); this.control.changeTracking.images.clear(); this.control.changeTracking.tables.clear(); this.control.changeTracking.lastScanTime = Date.now(); // Refresh our display this.refreshStats(); } } setupAutoRefresh() { if (this.control.autoRefreshInterval) { clearInterval(this.control.autoRefreshInterval); } this.control.autoRefreshInterval = setInterval(() => { if (this.control.isExpanded) { this.refreshStats(); } }, 30000); // 30 seconds } createControl() { return this.control.createControl(); } } // Export for global access window.StatusControl = StatusControl;