From 4dd48627d02cea15cc8ecc910527a9e1080d51cf Mon Sep 17 00:00:00 2001 From: Deco Vander Date: Fri, 4 Jul 2025 13:58:28 -0400 Subject: [PATCH] mplement proper async initialization for ProfanityFilter - Add async initialize() method for reliable initialization waiting - Add static create() factory method for easy async creation - Add initialization state tracking with isInitialized flag - Add warning system for methods called before full initialization - Update server.js to use proper async initialization pattern - Maintain backward compatibility with constructor-only usage - Add accessibility improvement for reduced motion preferences in CSS Fixes the race condition issue where consumers relied on arbitrary timeouts instead of properly waiting for async initialization to complete. --- profanity-filter.js | 87 ++++++++++++++++++++++++++++++++++++++--- public/style.css | 8 +++- server.js | 95 +++++++++++++++++++++++++++++---------------- 3 files changed, 150 insertions(+), 40 deletions(-) diff --git a/profanity-filter.js b/profanity-filter.js index b1a0df2..a5e694c 100644 --- a/profanity-filter.js +++ b/profanity-filter.js @@ -7,11 +7,23 @@ const sqlite3 = require('sqlite3').verbose(); const path = require('path'); class ProfanityFilter { + /** + * Static factory method for creating and initializing a ProfanityFilter + * @param {string|null} dbPath - Optional path to database file + * @returns {Promise} Fully initialized ProfanityFilter instance + */ + static async create(dbPath = null) { + const filter = new ProfanityFilter(dbPath); + await filter.initialize(); + return filter; + } + constructor(dbPath = null) { // Initialize separate database for profanity filter const defaultDbPath = path.join(__dirname, 'profanity.db'); this.dbPath = dbPath || defaultDbPath; this.db = new sqlite3.Database(this.dbPath); + this.isInitialized = false; // Base profanity words - comprehensive list this.baseProfanityWords = [ @@ -60,17 +72,66 @@ class ProfanityFilter { '%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/' }; - // Initialize database and load custom words + // Initialize custom words array this.customWords = []; - this.initializeDatabase(); - this.loadCustomWords(); - // Build patterns + // Build initial patterns with base words only this.patterns = this.buildPatterns(); } + + /** + * Async initialization method that must be called after construction + * @returns {Promise} + */ + async initialize() { + if (this.isInitialized) { + return; + } + + try { + // Initialize database synchronously first + await this.initializeDatabaseAsync(); + + // Load custom words from database + await this.loadCustomWords(); + + this.isInitialized = true; + console.log('ProfanityFilter initialization completed successfully'); + } catch (error) { + console.error('Error during ProfanityFilter initialization:', error); + throw error; + } + } /** - * Initialize the database table for custom profanity words + * Initialize the database table for custom profanity words (async version) + */ + async initializeDatabaseAsync() { + return new Promise((resolve, reject) => { + this.db.serialize(() => { + this.db.run(`CREATE TABLE IF NOT EXISTS profanity_words ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word TEXT NOT NULL UNIQUE, + severity TEXT DEFAULT 'medium', + category TEXT DEFAULT 'custom', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + created_by TEXT DEFAULT 'admin' + )`, (err) => { + if (err) { + console.error('Error creating profanity_words table:', err); + reject(err); + } else { + console.log('Profanity words table initialized successfully'); + resolve(); + } + }); + }); + }); + } + + /** + * Initialize the database table for custom profanity words (legacy sync version) + * @deprecated Use initializeDatabaseAsync() instead */ initializeDatabase() { this.db.serialize(() => { @@ -120,10 +181,22 @@ class ProfanityFilter { }); } + /** + * Check if the filter is fully initialized and warn if not + * @private + */ + _checkInitialization() { + if (!this.isInitialized) { + console.warn('⚠️ ProfanityFilter: Using base words only - custom words not loaded yet. Call initialize() or use ProfanityFilter.create() for full functionality.'); + } + } + /** * Get all profanity words (base + custom) */ getAllWords() { + this._checkInitialization(); + const baseWords = this.baseProfanityWords.map(word => ({ word: word.toLowerCase(), severity: this.getSeverity(word), @@ -234,6 +307,8 @@ class ProfanityFilter { containsProfanity(text) { if (!text || typeof text !== 'string') return false; + this._checkInitialization(); + const normalizedText = this.normalizeText(text); const originalText = text.toLowerCase(); @@ -258,6 +333,8 @@ class ProfanityFilter { }; } + this._checkInitialization(); + const normalizedText = this.normalizeText(text); const originalText = text.toLowerCase(); const matches = []; diff --git a/public/style.css b/public/style.css index 2323329..ad4a470 100644 --- a/public/style.css +++ b/public/style.css @@ -219,7 +219,13 @@ button[type="submit"]:hover { color: #721c24; border: 2px solid #dc3545; font-weight: bold; - animation: pulse 0.5s ease-in-out; +} + +/* Only animate for users who haven't requested reduced motion */ +@media (prefers-reduced-motion: no-preference) { + .message.error.profanity-rejection { + animation: pulse 0.5s ease-in-out; + } } [data-theme="dark"] .message.error.profanity-rejection { diff --git a/server.js b/server.js index 811ed17..7ae4bd4 100644 --- a/server.js +++ b/server.js @@ -25,18 +25,12 @@ const db = new sqlite3.Database('icewatch.db'); console.log('Database connection established'); -// Initialize profanity filter with its own database +// Initialize profanity filter with its own database (async) let profanityFilter; -try { - profanityFilter = new ProfanityFilter(); - console.log('Profanity filter initialized successfully with separate database'); -} catch (error) { - console.error('WARNING: Failed to initialize profanity filter:', error); - console.error('Creating fallback no-op profanity filter. ALL CONTENT WILL BE ALLOWED!'); - console.error('This is a security risk - please fix the profanity filter configuration.'); - - // Create a fallback no-op profanity filter that matches the full ProfanityFilter interface - profanityFilter = { + +// Create fallback filter function +function createFallbackFilter() { + return { // Core profanity checking methods checkText: () => ({ isProfane: false, reason: null }), containsProfanity: () => false, @@ -84,8 +78,21 @@ try { // Special property to identify this as a fallback filter _isFallback: true }; - - console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!'); +} + +// Initialize profanity filter asynchronously +async function initializeProfanityFilter() { + try { + profanityFilter = await ProfanityFilter.create(); + console.log('Profanity filter initialized successfully with separate database'); + } catch (error) { + console.error('WARNING: Failed to initialize profanity filter:', error); + console.error('Creating fallback no-op profanity filter. ALL CONTENT WILL BE ALLOWED!'); + console.error('This is a security risk - please fix the profanity filter configuration.'); + + profanityFilter = createFallbackFilter(); + console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!'); + } } // Initialize database @@ -175,28 +182,48 @@ function setupRoutes() { }); } -// Validate profanity filter is properly initialized before setting up routes - -// Initialize routes after everything is set up -setupRoutes(); - -// Start server -app.listen(PORT, () => { - console.log('==========================================='); - console.log('Great Lakes Ice Report server started'); - console.log(`Listening on port ${PORT}`); - console.log(`Visit http://localhost:${PORT} to view the website`); - - // Display profanity filter status - if (profanityFilter._isFallback) { - console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)'); - console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!'); - } else { - console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL'); +// Async server startup function +async function startServer() { + try { + // Initialize profanity filter first + await initializeProfanityFilter(); + + // Validate profanity filter is properly initialized + if (!profanityFilter) { + console.error('CRITICAL ERROR: profanityFilter is undefined after initialization attempt.'); + console.error('Cannot start server without a functional profanity filter.'); + process.exit(1); + } + + // Initialize routes after everything is set up + setupRoutes(); + + // Start server + app.listen(PORT, () => { + console.log('==========================================='); + console.log('Great Lakes Ice Report server started'); + console.log(`Listening on port ${PORT}`); + console.log(`Visit http://localhost:${PORT} to view the website`); + + // Display profanity filter status + if (profanityFilter._isFallback) { + console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)'); + console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!'); + } else { + console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL'); + } + + console.log('==========================================='); + }); + + } catch (error) { + console.error('CRITICAL ERROR: Failed to start server:', error); + process.exit(1); } - - console.log('==========================================='); -}); +} + +// Start the server +startServer(); // Graceful shutdown process.on('SIGINT', () => {