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.
This commit is contained in:
parent
542415cccd
commit
4dd48627d0
3 changed files with 150 additions and 40 deletions
|
@ -7,11 +7,23 @@ const sqlite3 = require('sqlite3').verbose();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
class ProfanityFilter {
|
class ProfanityFilter {
|
||||||
|
/**
|
||||||
|
* Static factory method for creating and initializing a ProfanityFilter
|
||||||
|
* @param {string|null} dbPath - Optional path to database file
|
||||||
|
* @returns {Promise<ProfanityFilter>} Fully initialized ProfanityFilter instance
|
||||||
|
*/
|
||||||
|
static async create(dbPath = null) {
|
||||||
|
const filter = new ProfanityFilter(dbPath);
|
||||||
|
await filter.initialize();
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(dbPath = null) {
|
constructor(dbPath = null) {
|
||||||
// Initialize separate database for profanity filter
|
// Initialize separate database for profanity filter
|
||||||
const defaultDbPath = path.join(__dirname, 'profanity.db');
|
const defaultDbPath = path.join(__dirname, 'profanity.db');
|
||||||
this.dbPath = dbPath || defaultDbPath;
|
this.dbPath = dbPath || defaultDbPath;
|
||||||
this.db = new sqlite3.Database(this.dbPath);
|
this.db = new sqlite3.Database(this.dbPath);
|
||||||
|
this.isInitialized = false;
|
||||||
|
|
||||||
// Base profanity words - comprehensive list
|
// Base profanity words - comprehensive list
|
||||||
this.baseProfanityWords = [
|
this.baseProfanityWords = [
|
||||||
|
@ -60,17 +72,66 @@ class ProfanityFilter {
|
||||||
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
|
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize database and load custom words
|
// Initialize custom words array
|
||||||
this.customWords = [];
|
this.customWords = [];
|
||||||
this.initializeDatabase();
|
|
||||||
this.loadCustomWords();
|
|
||||||
|
|
||||||
// Build patterns
|
// Build initial patterns with base words only
|
||||||
this.patterns = this.buildPatterns();
|
this.patterns = this.buildPatterns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async initialization method that must be called after construction
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
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() {
|
initializeDatabase() {
|
||||||
this.db.serialize(() => {
|
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)
|
* Get all profanity words (base + custom)
|
||||||
*/
|
*/
|
||||||
getAllWords() {
|
getAllWords() {
|
||||||
|
this._checkInitialization();
|
||||||
|
|
||||||
const baseWords = this.baseProfanityWords.map(word => ({
|
const baseWords = this.baseProfanityWords.map(word => ({
|
||||||
word: word.toLowerCase(),
|
word: word.toLowerCase(),
|
||||||
severity: this.getSeverity(word),
|
severity: this.getSeverity(word),
|
||||||
|
@ -234,6 +307,8 @@ class ProfanityFilter {
|
||||||
containsProfanity(text) {
|
containsProfanity(text) {
|
||||||
if (!text || typeof text !== 'string') return false;
|
if (!text || typeof text !== 'string') return false;
|
||||||
|
|
||||||
|
this._checkInitialization();
|
||||||
|
|
||||||
const normalizedText = this.normalizeText(text);
|
const normalizedText = this.normalizeText(text);
|
||||||
const originalText = text.toLowerCase();
|
const originalText = text.toLowerCase();
|
||||||
|
|
||||||
|
@ -258,6 +333,8 @@ class ProfanityFilter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._checkInitialization();
|
||||||
|
|
||||||
const normalizedText = this.normalizeText(text);
|
const normalizedText = this.normalizeText(text);
|
||||||
const originalText = text.toLowerCase();
|
const originalText = text.toLowerCase();
|
||||||
const matches = [];
|
const matches = [];
|
||||||
|
|
|
@ -219,7 +219,13 @@ button[type="submit"]:hover {
|
||||||
color: #721c24;
|
color: #721c24;
|
||||||
border: 2px solid #dc3545;
|
border: 2px solid #dc3545;
|
||||||
font-weight: bold;
|
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 {
|
[data-theme="dark"] .message.error.profanity-rejection {
|
||||||
|
|
95
server.js
95
server.js
|
@ -25,18 +25,12 @@ const db = new sqlite3.Database('icewatch.db');
|
||||||
|
|
||||||
console.log('Database connection established');
|
console.log('Database connection established');
|
||||||
|
|
||||||
// Initialize profanity filter with its own database
|
// Initialize profanity filter with its own database (async)
|
||||||
let profanityFilter;
|
let profanityFilter;
|
||||||
try {
|
|
||||||
profanityFilter = new ProfanityFilter();
|
// Create fallback filter function
|
||||||
console.log('Profanity filter initialized successfully with separate database');
|
function createFallbackFilter() {
|
||||||
} catch (error) {
|
return {
|
||||||
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 = {
|
|
||||||
// Core profanity checking methods
|
// Core profanity checking methods
|
||||||
checkText: () => ({ isProfane: false, reason: null }),
|
checkText: () => ({ isProfane: false, reason: null }),
|
||||||
containsProfanity: () => false,
|
containsProfanity: () => false,
|
||||||
|
@ -84,8 +78,21 @@ try {
|
||||||
// Special property to identify this as a fallback filter
|
// Special property to identify this as a fallback filter
|
||||||
_isFallback: true
|
_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
|
// Initialize database
|
||||||
|
@ -175,28 +182,48 @@ function setupRoutes() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate profanity filter is properly initialized before setting up routes
|
// Async server startup function
|
||||||
|
async function startServer() {
|
||||||
// Initialize routes after everything is set up
|
try {
|
||||||
setupRoutes();
|
// Initialize profanity filter first
|
||||||
|
await initializeProfanityFilter();
|
||||||
// Start server
|
|
||||||
app.listen(PORT, () => {
|
// Validate profanity filter is properly initialized
|
||||||
console.log('===========================================');
|
if (!profanityFilter) {
|
||||||
console.log('Great Lakes Ice Report server started');
|
console.error('CRITICAL ERROR: profanityFilter is undefined after initialization attempt.');
|
||||||
console.log(`Listening on port ${PORT}`);
|
console.error('Cannot start server without a functional profanity filter.');
|
||||||
console.log(`Visit http://localhost:${PORT} to view the website`);
|
process.exit(1);
|
||||||
|
}
|
||||||
// Display profanity filter status
|
|
||||||
if (profanityFilter._isFallback) {
|
// Initialize routes after everything is set up
|
||||||
console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)');
|
setupRoutes();
|
||||||
console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!');
|
|
||||||
} else {
|
// Start server
|
||||||
console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL');
|
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
|
// Graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue