mastercontroller 1.2.12 → 1.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CSPConfig.js +319 -0
- package/EventHandlerValidator.js +464 -0
- package/MasterAction.js +296 -72
- package/MasterBackendErrorHandler.js +769 -0
- package/MasterBenchmark.js +89 -0
- package/MasterBuildOptimizer.js +376 -0
- package/MasterBundleAnalyzer.js +108 -0
- package/MasterCache.js +400 -0
- package/MasterControl.js +76 -6
- package/MasterErrorHandler.js +487 -0
- package/MasterErrorLogger.js +360 -0
- package/MasterErrorMiddleware.js +407 -0
- package/MasterHtml.js +101 -14
- package/MasterMemoryMonitor.js +188 -0
- package/MasterProfiler.js +409 -0
- package/MasterRouter.js +273 -66
- package/MasterSanitizer.js +429 -0
- package/MasterTemplate.js +96 -3
- package/MasterValidator.js +546 -0
- package/README.md +0 -44
- package/SecurityMiddleware.js +486 -0
- package/SessionSecurity.js +416 -0
- package/package.json +3 -3
- package/ssr/ErrorBoundary.js +353 -0
- package/ssr/HTMLUtils.js +15 -0
- package/ssr/HydrationMismatch.js +265 -0
- package/ssr/PerformanceMonitor.js +233 -0
- package/ssr/SSRErrorHandler.js +273 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MasterErrorMiddleware - Request/Response error handling middleware
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { handleControllerError, handleRoutingError, sendErrorResponse } = require('./MasterBackendErrorHandler');
|
|
7
|
+
const { logger } = require('./MasterErrorLogger');
|
|
8
|
+
|
|
9
|
+
const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Global error handler middleware
|
|
13
|
+
* Wrap all controller actions with this
|
|
14
|
+
*/
|
|
15
|
+
function errorHandlerMiddleware(handler, controllerName, actionName) {
|
|
16
|
+
return async function wrappedHandler(requestObject) {
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Execute the actual handler
|
|
21
|
+
const result = await Promise.resolve(handler.call(this, requestObject));
|
|
22
|
+
|
|
23
|
+
// Log successful request in development
|
|
24
|
+
if (isDevelopment) {
|
|
25
|
+
const duration = Date.now() - startTime;
|
|
26
|
+
logger.debug({
|
|
27
|
+
code: 'MC_INFO_REQUEST_SUCCESS',
|
|
28
|
+
message: `${controllerName}#${actionName} completed`,
|
|
29
|
+
context: {
|
|
30
|
+
duration,
|
|
31
|
+
path: requestObject.pathName,
|
|
32
|
+
method: requestObject.type
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const duration = Date.now() - startTime;
|
|
41
|
+
|
|
42
|
+
// Handle the error
|
|
43
|
+
const mcError = handleControllerError(
|
|
44
|
+
error,
|
|
45
|
+
controllerName,
|
|
46
|
+
actionName,
|
|
47
|
+
requestObject.pathName
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Send error response
|
|
51
|
+
sendErrorResponse(
|
|
52
|
+
requestObject.response,
|
|
53
|
+
mcError,
|
|
54
|
+
requestObject.pathName
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Log to monitoring
|
|
58
|
+
logger.error({
|
|
59
|
+
code: mcError.code,
|
|
60
|
+
message: mcError.message,
|
|
61
|
+
controller: controllerName,
|
|
62
|
+
action: actionName,
|
|
63
|
+
route: requestObject.pathName,
|
|
64
|
+
method: requestObject.type,
|
|
65
|
+
duration,
|
|
66
|
+
originalError: error,
|
|
67
|
+
stack: error.stack
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Don't re-throw - error has been handled
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Request logging middleware
|
|
78
|
+
*/
|
|
79
|
+
function requestLoggerMiddleware() {
|
|
80
|
+
return function(requestObject, next) {
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
|
|
83
|
+
logger.info({
|
|
84
|
+
code: 'MC_INFO_REQUEST_START',
|
|
85
|
+
message: `${requestObject.type} ${requestObject.pathName}`,
|
|
86
|
+
context: {
|
|
87
|
+
method: requestObject.type,
|
|
88
|
+
path: requestObject.pathName,
|
|
89
|
+
params: requestObject.params,
|
|
90
|
+
query: requestObject.query,
|
|
91
|
+
ip: requestObject.request.connection?.remoteAddress
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Continue to next middleware
|
|
96
|
+
if (typeof next === 'function') {
|
|
97
|
+
next();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 404 handler middleware
|
|
104
|
+
*/
|
|
105
|
+
function notFoundMiddleware(requestObject) {
|
|
106
|
+
const mcError = handleRoutingError(
|
|
107
|
+
requestObject.pathName,
|
|
108
|
+
[] // Would need to pass available routes here
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
sendErrorResponse(
|
|
112
|
+
requestObject.response,
|
|
113
|
+
mcError,
|
|
114
|
+
requestObject.pathName
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract user code context from stack trace
|
|
120
|
+
*/
|
|
121
|
+
function extractUserCodeContext(stack) {
|
|
122
|
+
if (!stack) return null;
|
|
123
|
+
|
|
124
|
+
const lines = stack.split('\n');
|
|
125
|
+
const userFiles = [];
|
|
126
|
+
const frameworkFiles = [];
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
// Skip the error message line
|
|
130
|
+
if (!line.trim().startsWith('at ')) continue;
|
|
131
|
+
|
|
132
|
+
// Extract file path from stack line
|
|
133
|
+
const match = line.match(/\((.+?):(\d+):(\d+)\)|at (.+?):(\d+):(\d+)/);
|
|
134
|
+
if (!match) continue;
|
|
135
|
+
|
|
136
|
+
const filePath = match[1] || match[4];
|
|
137
|
+
const lineNum = match[2] || match[5];
|
|
138
|
+
const colNum = match[3] || match[6];
|
|
139
|
+
|
|
140
|
+
if (!filePath) continue;
|
|
141
|
+
|
|
142
|
+
// Categorize as user code or framework code
|
|
143
|
+
const isFramework = filePath.includes('node_modules/mastercontroller');
|
|
144
|
+
const isNodeInternal = filePath.includes('node:internal') || filePath.includes('/lib/internal/');
|
|
145
|
+
|
|
146
|
+
if (isNodeInternal) continue;
|
|
147
|
+
|
|
148
|
+
const fileInfo = {
|
|
149
|
+
file: filePath,
|
|
150
|
+
line: lineNum,
|
|
151
|
+
column: colNum,
|
|
152
|
+
location: `${filePath}:${lineNum}:${colNum}`
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (isFramework) {
|
|
156
|
+
frameworkFiles.push(fileInfo);
|
|
157
|
+
} else {
|
|
158
|
+
userFiles.push(fileInfo);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
userFiles,
|
|
164
|
+
frameworkFiles,
|
|
165
|
+
triggeringFile: userFiles[0] || frameworkFiles[0] || null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Uncaught exception handler
|
|
171
|
+
*/
|
|
172
|
+
function setupGlobalErrorHandlers() {
|
|
173
|
+
// Handle uncaught exceptions
|
|
174
|
+
process.on('uncaughtException', (error) => {
|
|
175
|
+
console.error('[MasterController] Uncaught Exception:', error);
|
|
176
|
+
|
|
177
|
+
// Extract context from stack trace
|
|
178
|
+
const context = extractUserCodeContext(error.stack);
|
|
179
|
+
|
|
180
|
+
// Build enhanced error message
|
|
181
|
+
let enhancedMessage = `Uncaught exception: ${error.message}`;
|
|
182
|
+
|
|
183
|
+
if (context && context.triggeringFile) {
|
|
184
|
+
enhancedMessage += `\n\n🔍 Error Location: ${context.triggeringFile.location}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (context && context.userFiles.length > 0) {
|
|
188
|
+
enhancedMessage += `\n\n📂 Your Code Involved:`;
|
|
189
|
+
context.userFiles.forEach((file, i) => {
|
|
190
|
+
if (i < 3) { // Show first 3 user files
|
|
191
|
+
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (context && context.frameworkFiles.length > 0) {
|
|
197
|
+
enhancedMessage += `\n\n🔧 Framework Files Involved:`;
|
|
198
|
+
context.frameworkFiles.forEach((file, i) => {
|
|
199
|
+
if (i < 2) { // Show first 2 framework files
|
|
200
|
+
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.error(enhancedMessage);
|
|
206
|
+
|
|
207
|
+
logger.fatal({
|
|
208
|
+
code: 'MC_ERR_UNCAUGHT_EXCEPTION',
|
|
209
|
+
message: enhancedMessage,
|
|
210
|
+
originalError: error,
|
|
211
|
+
stack: error.stack,
|
|
212
|
+
context: context
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Give logger time to write, then exit
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}, 1000);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Handle unhandled promise rejections
|
|
222
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
223
|
+
console.error('[MasterController] Unhandled Rejection:', reason);
|
|
224
|
+
|
|
225
|
+
// Extract context from stack trace if available
|
|
226
|
+
const context = reason?.stack ? extractUserCodeContext(reason.stack) : null;
|
|
227
|
+
|
|
228
|
+
// Build enhanced error message
|
|
229
|
+
let enhancedMessage = `Unhandled promise rejection: ${reason}`;
|
|
230
|
+
|
|
231
|
+
if (context && context.triggeringFile) {
|
|
232
|
+
enhancedMessage += `\n\n🔍 Error Location: ${context.triggeringFile.location}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (context && context.userFiles.length > 0) {
|
|
236
|
+
enhancedMessage += `\n\n📂 Your Code Involved:`;
|
|
237
|
+
context.userFiles.forEach((file, i) => {
|
|
238
|
+
if (i < 3) { // Show first 3 user files
|
|
239
|
+
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (enhancedMessage !== `Unhandled promise rejection: ${reason}`) {
|
|
245
|
+
console.error(enhancedMessage);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logger.error({
|
|
249
|
+
code: 'MC_ERR_UNHANDLED_REJECTION',
|
|
250
|
+
message: enhancedMessage,
|
|
251
|
+
originalError: reason,
|
|
252
|
+
stack: reason?.stack,
|
|
253
|
+
context: context
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Handle warnings
|
|
258
|
+
process.on('warning', (warning) => {
|
|
259
|
+
if (isDevelopment) {
|
|
260
|
+
console.warn('[MasterController] Warning:', warning);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
logger.warn({
|
|
264
|
+
code: 'MC_WARN_PROCESS_WARNING',
|
|
265
|
+
message: warning.message,
|
|
266
|
+
context: {
|
|
267
|
+
name: warning.name,
|
|
268
|
+
stack: warning.stack
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Safe file reader with error handling
|
|
276
|
+
*/
|
|
277
|
+
function safeReadFile(fs, filePath, encoding = 'utf8') {
|
|
278
|
+
try {
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
content: fs.readFileSync(filePath, encoding),
|
|
282
|
+
error: null
|
|
283
|
+
};
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const { handleFileReadError } = require('./MasterBackendErrorHandler');
|
|
286
|
+
const mcError = handleFileReadError(error, filePath);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
content: null,
|
|
291
|
+
error: mcError
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Safe file existence check
|
|
298
|
+
*/
|
|
299
|
+
function safeFileExists(fs, filePath) {
|
|
300
|
+
try {
|
|
301
|
+
return fs.existsSync(filePath);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
logger.warn({
|
|
304
|
+
code: 'MC_WARN_FILE_CHECK',
|
|
305
|
+
message: `Could not check if file exists: ${filePath}`,
|
|
306
|
+
originalError: error
|
|
307
|
+
});
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Wrap controller class with error handling
|
|
314
|
+
*/
|
|
315
|
+
function wrapController(ControllerClass, controllerName) {
|
|
316
|
+
const wrappedMethods = {};
|
|
317
|
+
|
|
318
|
+
// Get all methods from the controller
|
|
319
|
+
const methodNames = Object.getOwnPropertyNames(ControllerClass.prototype);
|
|
320
|
+
|
|
321
|
+
methodNames.forEach(methodName => {
|
|
322
|
+
if (methodName === 'constructor') return;
|
|
323
|
+
|
|
324
|
+
const originalMethod = ControllerClass.prototype[methodName];
|
|
325
|
+
|
|
326
|
+
if (typeof originalMethod === 'function') {
|
|
327
|
+
// Wrap each method with error handling
|
|
328
|
+
wrappedMethods[methodName] = errorHandlerMiddleware(
|
|
329
|
+
originalMethod,
|
|
330
|
+
controllerName,
|
|
331
|
+
methodName
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Create new class with wrapped methods
|
|
337
|
+
const WrappedController = class extends ControllerClass {
|
|
338
|
+
constructor(...args) {
|
|
339
|
+
super(...args);
|
|
340
|
+
|
|
341
|
+
// Apply wrapped methods
|
|
342
|
+
Object.keys(wrappedMethods).forEach(methodName => {
|
|
343
|
+
this[methodName] = wrappedMethods[methodName].bind(this);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return WrappedController;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Performance tracking middleware
|
|
353
|
+
*/
|
|
354
|
+
function performanceMiddleware() {
|
|
355
|
+
const requests = new Map();
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
start(requestId, requestObject) {
|
|
359
|
+
requests.set(requestId, {
|
|
360
|
+
startTime: Date.now(),
|
|
361
|
+
path: requestObject.pathName,
|
|
362
|
+
method: requestObject.type
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
end(requestId) {
|
|
367
|
+
const req = requests.get(requestId);
|
|
368
|
+
if (!req) return;
|
|
369
|
+
|
|
370
|
+
const duration = Date.now() - req.startTime;
|
|
371
|
+
|
|
372
|
+
if (duration > 1000) {
|
|
373
|
+
logger.warn({
|
|
374
|
+
code: 'MC_WARN_SLOW_REQUEST',
|
|
375
|
+
message: `Slow request detected (${duration}ms)`,
|
|
376
|
+
context: {
|
|
377
|
+
duration,
|
|
378
|
+
path: req.path,
|
|
379
|
+
method: req.method
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
requests.delete(requestId);
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
getStats() {
|
|
388
|
+
return {
|
|
389
|
+
activeRequests: requests.size,
|
|
390
|
+
requests: Array.from(requests.values())
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const performanceTracker = performanceMiddleware();
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
errorHandlerMiddleware,
|
|
400
|
+
requestLoggerMiddleware,
|
|
401
|
+
notFoundMiddleware,
|
|
402
|
+
setupGlobalErrorHandlers,
|
|
403
|
+
safeReadFile,
|
|
404
|
+
safeFileExists,
|
|
405
|
+
wrapController,
|
|
406
|
+
performanceTracker
|
|
407
|
+
};
|
package/MasterHtml.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version 0.0.
|
|
1
|
+
// version 0.0.24
|
|
2
2
|
|
|
3
3
|
var master = require('./MasterControl');
|
|
4
4
|
var fs = require('fs');
|
|
@@ -7,6 +7,14 @@ var toolClass = require('./MasterTools');
|
|
|
7
7
|
var temp = new tempClass();
|
|
8
8
|
var tools = new toolClass();
|
|
9
9
|
|
|
10
|
+
// Enhanced error handling
|
|
11
|
+
const { handleTemplateError } = require('./MasterBackendErrorHandler');
|
|
12
|
+
const { safeReadFile, safeFileExists } = require('./MasterErrorMiddleware');
|
|
13
|
+
const { logger } = require('./MasterErrorLogger');
|
|
14
|
+
|
|
15
|
+
// Security - Sanitization
|
|
16
|
+
const { sanitizeTemplateHTML, sanitizeUserHTML, escapeHTML } = require('./MasterSanitizer');
|
|
17
|
+
|
|
10
18
|
class html {
|
|
11
19
|
|
|
12
20
|
javaScriptSerializer(name, obj){
|
|
@@ -17,21 +25,41 @@ class html {
|
|
|
17
25
|
|
|
18
26
|
// render partial views
|
|
19
27
|
renderPartial(path, data){
|
|
20
|
-
|
|
28
|
+
try {
|
|
29
|
+
var partialViewUrl = `/app/views/${path}`;
|
|
30
|
+
var fullPath = master.router.currentRoute.root + partialViewUrl;
|
|
31
|
+
|
|
32
|
+
const fileResult = safeReadFile(fs, fullPath);
|
|
33
|
+
|
|
34
|
+
if (!fileResult.success) {
|
|
35
|
+
logger.warn({
|
|
36
|
+
code: 'MC_ERR_VIEW_NOT_FOUND',
|
|
37
|
+
message: `Partial view not found: ${path}`,
|
|
38
|
+
file: fullPath
|
|
39
|
+
});
|
|
40
|
+
return `<!-- Partial view not found: ${path} -->`;
|
|
41
|
+
}
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
var partialView = null;
|
|
44
|
+
if(master.overwrite.isTemplate){
|
|
45
|
+
partialView = master.overwrite.templateRender(data, "renderPartialView");
|
|
46
|
+
}
|
|
47
|
+
else{
|
|
48
|
+
partialView = temp.htmlBuilder(fileResult.content, data);
|
|
49
|
+
}
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
return partialView;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const mcError = handleTemplateError(error, path, data);
|
|
54
|
+
logger.error({
|
|
55
|
+
code: mcError.code,
|
|
56
|
+
message: mcError.message,
|
|
57
|
+
file: path,
|
|
58
|
+
originalError: error
|
|
59
|
+
});
|
|
60
|
+
return `<!-- Error rendering partial: ${path} -->`;
|
|
31
61
|
}
|
|
32
62
|
|
|
33
|
-
return partialView;
|
|
34
|
-
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
// render all your link tags styles given the folder location
|
|
@@ -447,7 +475,7 @@ class html {
|
|
|
447
475
|
return rangeField;
|
|
448
476
|
}
|
|
449
477
|
|
|
450
|
-
// allows you to add data object to params
|
|
478
|
+
// allows you to add data object to params
|
|
451
479
|
addDataToParams(data){
|
|
452
480
|
|
|
453
481
|
//loop through data and add it to new oobjects prototype
|
|
@@ -457,7 +485,66 @@ class html {
|
|
|
457
485
|
master.view.extend(newObj);
|
|
458
486
|
}
|
|
459
487
|
}
|
|
460
|
-
|
|
488
|
+
|
|
489
|
+
// ==================== Security Methods ====================
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Sanitize user-generated HTML content
|
|
493
|
+
* Use this for any HTML that comes from user input
|
|
494
|
+
* @param {string} html - HTML content to sanitize
|
|
495
|
+
* @returns {string} - Sanitized HTML
|
|
496
|
+
*/
|
|
497
|
+
sanitizeHTML(html) {
|
|
498
|
+
return sanitizeUserHTML(html);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Escape HTML special characters
|
|
503
|
+
* Use this to display user input as text (not HTML)
|
|
504
|
+
* @param {string} text - Text to escape
|
|
505
|
+
* @returns {string} - Escaped text safe for display
|
|
506
|
+
*/
|
|
507
|
+
escapeHTML(text) {
|
|
508
|
+
return escapeHTML(text);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Render user content safely
|
|
513
|
+
* Sanitizes HTML and wraps in container
|
|
514
|
+
* @param {string} content - User-generated content
|
|
515
|
+
* @param {string} containerTag - HTML tag to wrap content (default: div)
|
|
516
|
+
* @param {object} attrs - Attributes for container
|
|
517
|
+
* @returns {string} - Safe HTML
|
|
518
|
+
*/
|
|
519
|
+
renderUserContent(content, containerTag = 'div', attrs = {}) {
|
|
520
|
+
const sanitized = sanitizeUserHTML(content);
|
|
521
|
+
|
|
522
|
+
let attrStr = '';
|
|
523
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
524
|
+
attrStr += ` ${key}="${escapeHTML(String(value))}"`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return `<${containerTag}${attrStr}>${sanitized}</${containerTag}>`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Create safe text node content
|
|
532
|
+
* @param {string} text - Text content
|
|
533
|
+
* @returns {string} - HTML-escaped text
|
|
534
|
+
*/
|
|
535
|
+
textNode(text) {
|
|
536
|
+
return escapeHTML(text);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Create safe attribute value
|
|
541
|
+
* @param {string} value - Attribute value
|
|
542
|
+
* @returns {string} - Escaped and quoted value
|
|
543
|
+
*/
|
|
544
|
+
safeAttr(value) {
|
|
545
|
+
return `"${escapeHTML(String(value))}"`;
|
|
546
|
+
}
|
|
547
|
+
|
|
461
548
|
}
|
|
462
549
|
|
|
463
550
|
master.extendView("html", html);
|