/** * 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 = `