vako 1.3.0

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.
@@ -0,0 +1,834 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ class LayoutManager {
5
+ constructor(app, options = {}) {
6
+ this.app = app;
7
+ this.options = {
8
+ defaultLayout: 'default',
9
+ layoutsDir: 'views/layouts',
10
+ extension: '.ejs',
11
+ ...options
12
+ };
13
+ this.layoutCache = new Map();
14
+ this.layoutSections = new Map();
15
+
16
+ // Configuration de sécurité
17
+ this.securityConfig = {
18
+ maxCacheSize: 1000,
19
+ maxSectionSize: 50000, // 50KB par section
20
+ allowedExtensions: ['.ejs', '.html'],
21
+ maxNestingDepth: 10
22
+ };
23
+
24
+ // Initialisation du nettoyage automatique du cache
25
+ this.initCacheCleanup();
26
+ }
27
+
28
+ /**
29
+ * Middleware principal pour la gestion des layouts
30
+ */
31
+ middleware() {
32
+ return (req, res, next) => {
33
+ const originalRender = res.render;
34
+
35
+ // Wrapper sécurisé pour res.render
36
+ res.render = this.createSecureRenderWrapper(originalRender, req, res);
37
+
38
+ // Helpers de layout
39
+ res.locals.layout = this.createLayoutHelpers(req, res);
40
+
41
+ next();
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Créer un wrapper sécurisé pour res.render
47
+ */
48
+ createSecureRenderWrapper(originalRender, req, res) {
49
+ return (view, options = {}, callback) => {
50
+ try {
51
+ // Validation des paramètres
52
+ this.validateRenderParameters(view, options);
53
+
54
+ // Bypass du layout si demandé
55
+ if (options.layout === false) {
56
+ return originalRender.call(res, view, options, callback);
57
+ }
58
+
59
+ const layoutName = this.sanitizeLayoutName(options.layout || this.options.defaultLayout);
60
+ const layoutData = this.prepareLayoutData(view, options, req);
61
+
62
+ this.renderWithLayout(res, view, layoutName, layoutData, originalRender, callback);
63
+ } catch (error) {
64
+ this.handleRenderError(error, res, view, options, originalRender, callback);
65
+ }
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Validation sécurisée des paramètres de rendu
71
+ */
72
+ validateRenderParameters(view, options) {
73
+ if (!view || typeof view !== 'string') {
74
+ throw new Error('View name must be a non-empty string');
75
+ }
76
+
77
+ if (view.length > 255) {
78
+ throw new Error('View name too long');
79
+ }
80
+
81
+ // Validation contre la traversée de répertoire
82
+ if (view.includes('..') || view.includes('\\') || view.startsWith('/')) {
83
+ throw new Error('Invalid view name: path traversal detected');
84
+ }
85
+
86
+ if (options && typeof options !== 'object') {
87
+ throw new Error('Options must be an object');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Nettoyage sécurisé du nom de layout
93
+ */
94
+ sanitizeLayoutName(layoutName) {
95
+ if (!layoutName || typeof layoutName !== 'string') {
96
+ return this.options.defaultLayout;
97
+ }
98
+
99
+ // Supprimer les caractères dangereux
100
+ const sanitized = layoutName
101
+ .replace(/[^a-zA-Z0-9\-_]/g, '')
102
+ .substring(0, 50);
103
+
104
+ return sanitized || this.options.defaultLayout;
105
+ }
106
+
107
+ /**
108
+ * Préparation sécurisée des données de layout
109
+ */
110
+ prepareLayoutData(view, options, req) {
111
+ const baseData = {
112
+ view: this.sanitizeViewName(view),
113
+ sections: this.sanitizeSections(options.sections || {}),
114
+ meta: this.sanitizeMetaData(options.meta),
115
+ layout: this.sanitizeLayoutOptions(options.layout),
116
+ request: this.extractSafeRequestData(req)
117
+ };
118
+
119
+ // Fusion sécurisée des options
120
+ return this.mergeOptionsSecurely(baseData, options);
121
+ }
122
+
123
+ /**
124
+ * Nettoyage des sections
125
+ */
126
+ sanitizeSections(sections) {
127
+ const sanitized = {};
128
+
129
+ for (const [key, value] of Object.entries(sections)) {
130
+ if (typeof key === 'string' && key.length <= 50) {
131
+ const cleanKey = key.replace(/[^a-zA-Z0-9\-_]/g, '');
132
+
133
+ if (typeof value === 'string' && value.length <= this.securityConfig.maxSectionSize) {
134
+ sanitized[cleanKey] = value;
135
+ }
136
+ }
137
+ }
138
+
139
+ return sanitized;
140
+ }
141
+
142
+ /**
143
+ * Nettoyage des métadonnées
144
+ */
145
+ sanitizeMetaData(meta = {}) {
146
+ const allowedFields = ['title', 'description', 'keywords', 'author', 'viewport'];
147
+ const sanitized = {};
148
+
149
+ allowedFields.forEach(field => {
150
+ if (meta[field] && typeof meta[field] === 'string') {
151
+ sanitized[field] = meta[field].substring(0, 200);
152
+ }
153
+ });
154
+
155
+ return {
156
+ title: sanitized.title || 'Veko.js App',
157
+ description: sanitized.description || '',
158
+ keywords: sanitized.keywords || '',
159
+ author: sanitized.author || '',
160
+ viewport: sanitized.viewport || 'width=device-width, initial-scale=1.0'
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Extraction sécurisée des données de requête
166
+ */
167
+ extractSafeRequestData(req) {
168
+ return {
169
+ url: req.url || '',
170
+ path: req.path || '',
171
+ method: req.method || 'GET',
172
+ query: this.sanitizeQueryParams(req.query),
173
+ params: this.sanitizeParams(req.params)
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Rendu avec layout de manière asynchrone et sécurisée
179
+ */
180
+ async renderWithLayout(res, view, layoutName, data, originalRender, callback) {
181
+ try {
182
+ // Vérification du cache
183
+ const cacheKey = `${view}:${layoutName}`;
184
+
185
+ // Rendu de la vue en contenu
186
+ const content = await this.renderViewToString(view, data);
187
+ data.sections.content = content;
188
+
189
+ const layoutPath = this.getLayoutPath(layoutName);
190
+
191
+ if (await this.layoutExists(layoutPath)) {
192
+ originalRender.call(res, layoutPath, data, callback);
193
+ } else {
194
+ await this.createDefaultLayoutAsync(layoutName);
195
+ originalRender.call(res, layoutPath, data, callback);
196
+ }
197
+ } catch (error) {
198
+ this.handleRenderError(error, res, view, data, originalRender, callback);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Rendu de vue en chaîne de caractères avec gestion d'erreur
204
+ */
205
+ async renderViewToString(view, data) {
206
+ return new Promise((resolve, reject) => {
207
+ const viewPath = this.resolveViewPath(view);
208
+
209
+ // Vérification sécurisée de l'existence du fichier
210
+ this.validateViewPath(viewPath)
211
+ .then(() => {
212
+ try {
213
+ const ejs = require('ejs');
214
+ const template = fs.readFileSync(viewPath, 'utf8');
215
+
216
+ // Options sécurisées pour EJS
217
+ const ejsOptions = {
218
+ filename: viewPath,
219
+ rmWhitespace: true,
220
+ escape: (str) => this.escapeHtml(str)
221
+ };
222
+
223
+ const html = ejs.render(template, data, ejsOptions);
224
+ resolve(html);
225
+ } catch (error) {
226
+ reject(new Error(`Template rendering failed: ${error.message}`));
227
+ }
228
+ })
229
+ .catch(reject);
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Validation asynchrone du chemin de vue
235
+ */
236
+ async validateViewPath(viewPath) {
237
+ return new Promise((resolve, reject) => {
238
+ fs.access(viewPath, fs.constants.R_OK, (err) => {
239
+ if (err) {
240
+ reject(new Error(`View not accessible: ${path.basename(viewPath)}`));
241
+ } else {
242
+ resolve();
243
+ }
244
+ });
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Résolution sécurisée du chemin de vue
250
+ */
251
+ resolveViewPath(view) {
252
+ const viewsDir = path.resolve(process.cwd(), this.app.options.viewsDir || 'views');
253
+ let viewPath = path.join(viewsDir, view);
254
+
255
+ // Validation contre la traversée de répertoire
256
+ if (!viewPath.startsWith(viewsDir)) {
257
+ throw new Error('Invalid view path: outside views directory');
258
+ }
259
+
260
+ if (!viewPath.endsWith('.ejs')) {
261
+ viewPath += '.ejs';
262
+ }
263
+
264
+ return viewPath;
265
+ }
266
+
267
+ /**
268
+ * Obtenir le chemin du layout de manière sécurisée
269
+ */
270
+ getLayoutPath(layoutName) {
271
+ const layoutsDir = path.resolve(process.cwd(), this.options.layoutsDir);
272
+ let layoutPath = path.join(layoutsDir, layoutName);
273
+
274
+ // Validation contre la traversée de répertoire
275
+ if (!layoutPath.startsWith(layoutsDir)) {
276
+ throw new Error('Invalid layout path: outside layouts directory');
277
+ }
278
+
279
+ if (!layoutPath.endsWith(this.options.extension)) {
280
+ layoutPath += this.options.extension;
281
+ }
282
+
283
+ return layoutPath;
284
+ }
285
+
286
+ /**
287
+ * Vérification asynchrone de l'existence d'un layout
288
+ */
289
+ async layoutExists(layoutPath) {
290
+ return new Promise((resolve) => {
291
+ fs.access(layoutPath, fs.constants.R_OK, (err) => {
292
+ resolve(!err);
293
+ });
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Création d'helpers de layout sécurisés
299
+ */
300
+ createLayoutHelpers(req, res) {
301
+ return {
302
+ section: (name, content) => {
303
+ if (!res.locals.sections) res.locals.sections = {};
304
+
305
+ // Validation du nom de section
306
+ const safeName = this.sanitizeSectionName(name);
307
+ const safeContent = this.sanitizeSectionContent(content);
308
+
309
+ res.locals.sections[safeName] = safeContent;
310
+ return '';
311
+ },
312
+
313
+ css: (href) => {
314
+ if (!res.locals.css) res.locals.css = [];
315
+
316
+ // Validation de l'URL CSS
317
+ const safeHref = this.sanitizeResourceUrl(href);
318
+ if (safeHref && res.locals.css.length < 20) {
319
+ res.locals.css.push(safeHref);
320
+ }
321
+ return '';
322
+ },
323
+
324
+ js: (src) => {
325
+ if (!res.locals.js) res.locals.js = [];
326
+
327
+ // Validation de l'URL JavaScript
328
+ const safeSrc = this.sanitizeResourceUrl(src);
329
+ if (safeSrc && res.locals.js.length < 20) {
330
+ res.locals.js.push(safeSrc);
331
+ }
332
+ return '';
333
+ },
334
+
335
+ title: (title) => {
336
+ res.locals.title = this.sanitizeText(title, 100);
337
+ return '';
338
+ },
339
+
340
+ meta: (name, content) => {
341
+ if (!res.locals.meta) res.locals.meta = {};
342
+
343
+ const safeName = this.sanitizeText(name, 50);
344
+ const safeContent = this.sanitizeText(content, 200);
345
+
346
+ if (safeName && safeContent) {
347
+ res.locals.meta[safeName] = safeContent;
348
+ }
349
+ return '';
350
+ }
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Création asynchrone du layout par défaut
356
+ */
357
+ async createDefaultLayoutAsync(layoutName) {
358
+ return new Promise((resolve, reject) => {
359
+ const layoutsDir = path.join(process.cwd(), this.options.layoutsDir);
360
+
361
+ // Création du répertoire s'il n'existe pas
362
+ fs.mkdir(layoutsDir, { recursive: true }, (err) => {
363
+ if (err && err.code !== 'EEXIST') {
364
+ reject(err);
365
+ return;
366
+ }
367
+
368
+ const layoutPath = this.getLayoutPath(layoutName);
369
+
370
+ // Vérification si le layout existe déjà
371
+ fs.access(layoutPath, fs.constants.F_OK, (err) => {
372
+ if (!err) {
373
+ resolve(); // Le layout existe déjà
374
+ return;
375
+ }
376
+
377
+ // Création du layout
378
+ const defaultContent = this.generateDefaultLayoutContent();
379
+ fs.writeFile(layoutPath, defaultContent, 'utf8', (writeErr) => {
380
+ if (writeErr) {
381
+ reject(writeErr);
382
+ } else {
383
+ const relativePath = path.relative(process.cwd(), layoutPath);
384
+ this.app.logger?.log('create', 'Default layout created', `📄 ${relativePath}`);
385
+ resolve();
386
+ }
387
+ });
388
+ });
389
+ });
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Génération du contenu de layout par défaut optimisé
395
+ */
396
+ generateDefaultLayoutContent() {
397
+ return `<!DOCTYPE html>
398
+ <html lang="fr">
399
+ <head>
400
+ <meta charset="UTF-8">
401
+ <meta name="viewport" content="<%= meta.viewport %>">
402
+ <title><%= meta.title %></title>
403
+
404
+ <% if (meta.description) { %>
405
+ <meta name="description" content="<%= meta.description %>">
406
+ <% } %>
407
+
408
+ <% if (meta.keywords) { %>
409
+ <meta name="keywords" content="<%= meta.keywords %>">
410
+ <% } %>
411
+
412
+ <% if (meta.author) { %>
413
+ <meta name="author" content="<%= meta.author %>">
414
+ <% } %>
415
+
416
+ <!-- CSS par défaut avec CSP -->
417
+ <style nonce="<%= locals.nonce || '' %>">
418
+ * {
419
+ box-sizing: border-box;
420
+ }
421
+
422
+ body {
423
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
424
+ margin: 0;
425
+ padding: 0;
426
+ line-height: 1.6;
427
+ color: #333;
428
+ background-color: #fff;
429
+ }
430
+
431
+ .container {
432
+ max-width: 1200px;
433
+ margin: 0 auto;
434
+ padding: 1rem;
435
+ }
436
+
437
+ header {
438
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
439
+ color: white;
440
+ padding: 1rem 0;
441
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
442
+ }
443
+
444
+ main {
445
+ min-height: 60vh;
446
+ padding: 2rem 0;
447
+ }
448
+
449
+ footer {
450
+ background: #333;
451
+ color: white;
452
+ text-align: center;
453
+ padding: 1rem 0;
454
+ margin-top: 2rem;
455
+ }
456
+
457
+ @media (max-width: 768px) {
458
+ .container {
459
+ padding: 0.5rem;
460
+ }
461
+ }
462
+ </style>
463
+
464
+ <!-- CSS personnalisé -->
465
+ <% if (locals.css && Array.isArray(locals.css)) { %>
466
+ <% locals.css.forEach(href => { %>
467
+ <link rel="stylesheet" href="<%= href %>" crossorigin="anonymous">
468
+ <% }); %>
469
+ <% } %>
470
+
471
+ <!-- Section head personnalisée -->
472
+ <% if (sections && sections.head) { %>
473
+ <%- sections.head %>
474
+ <% } %>
475
+ </head>
476
+ <body class="<%= locals.bodyClass || '' %>">
477
+ <!-- Header -->
478
+ <% if (sections && sections.header) { %>
479
+ <header>
480
+ <div class="container">
481
+ <%- sections.header %>
482
+ </div>
483
+ </header>
484
+ <% } else { %>
485
+ <header>
486
+ <div class="container">
487
+ <h1>🚀 Veko.js</h1>
488
+ <p>Ultra modern Node.js framework</p>
489
+ </div>
490
+ </header>
491
+ <% } %>
492
+
493
+ <!-- Contenu principal -->
494
+ <main>
495
+ <div class="container">
496
+ <%- sections.content || '' %>
497
+ </div>
498
+ </main>
499
+
500
+ <!-- Footer -->
501
+ <% if (sections && sections.footer) { %>
502
+ <footer>
503
+ <div class="container">
504
+ <%- sections.footer %>
505
+ </div>
506
+ </footer>
507
+ <% } else { %>
508
+ <footer>
509
+ <div class="container">
510
+ <p>Powered by Veko.js ⚡</p>
511
+ </div>
512
+ </footer>
513
+ <% } %>
514
+
515
+ <!-- JavaScript -->
516
+ <% if (locals.js && Array.isArray(locals.js)) { %>
517
+ <% locals.js.forEach(src => { %>
518
+ <script src="<%= src %>" crossorigin="anonymous"></script>
519
+ <% }); %>
520
+ <% } %>
521
+
522
+ <!-- Section scripts personnalisée -->
523
+ <% if (sections && sections.scripts) { %>
524
+ <%- sections.scripts %>
525
+ <% } %>
526
+ </body>
527
+ </html>`;
528
+ }
529
+
530
+ /**
531
+ * Création de layout avec validation améliorée
532
+ */
533
+ async createLayout(layoutName, content = null) {
534
+ try {
535
+ const sanitizedName = this.sanitizeLayoutName(layoutName);
536
+ const layoutPath = this.getLayoutPath(sanitizedName);
537
+ const layoutsDir = path.dirname(layoutPath);
538
+
539
+ // Création du répertoire de manière asynchrone
540
+ await fs.promises.mkdir(layoutsDir, { recursive: true });
541
+
542
+ const layoutContent = content || this.generateDefaultLayoutContent();
543
+
544
+ // Validation du contenu
545
+ if (typeof layoutContent !== 'string' || layoutContent.length > 100000) {
546
+ throw new Error('Invalid layout content');
547
+ }
548
+
549
+ await fs.promises.writeFile(layoutPath, layoutContent, 'utf8');
550
+
551
+ const relativePath = path.relative(process.cwd(), layoutPath);
552
+ this.app.logger?.log('create', 'Layout created', `📄 ${relativePath}`);
553
+
554
+ return this.app;
555
+ } catch (error) {
556
+ this.app.logger?.log('error', 'Error creating layout', error.message);
557
+ throw error;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Suppression sécurisée de layout
563
+ */
564
+ async deleteLayout(layoutName) {
565
+ try {
566
+ const sanitizedName = this.sanitizeLayoutName(layoutName);
567
+ const layoutPath = this.getLayoutPath(sanitizedName);
568
+
569
+ await fs.promises.access(layoutPath, fs.constants.F_OK);
570
+ await fs.promises.unlink(layoutPath);
571
+
572
+ // Nettoyage du cache
573
+ this.layoutCache.delete(sanitizedName);
574
+
575
+ const relativePath = path.relative(process.cwd(), layoutPath);
576
+ this.app.logger?.log('delete', 'Layout deleted', `📄 ${relativePath}`);
577
+
578
+ return this.app;
579
+ } catch (error) {
580
+ if (error.code === 'ENOENT') {
581
+ this.app.logger?.log('warning', 'Layout not found', `📄 ${layoutName}`);
582
+ } else {
583
+ this.app.logger?.log('error', 'Error deleting layout', error.message);
584
+ throw error;
585
+ }
586
+ return this.app;
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Liste des layouts avec gestion d'erreur
592
+ */
593
+ async listLayouts() {
594
+ try {
595
+ const layoutsDir = path.join(process.cwd(), this.options.layoutsDir);
596
+
597
+ await fs.promises.access(layoutsDir, fs.constants.R_OK);
598
+ const files = await fs.promises.readdir(layoutsDir);
599
+
600
+ return files
601
+ .filter(file => this.securityConfig.allowedExtensions.includes(path.extname(file)))
602
+ .map(file => path.basename(file, this.options.extension))
603
+ .filter(name => name.length > 0);
604
+ } catch (error) {
605
+ if (error.code === 'ENOENT') {
606
+ return [];
607
+ }
608
+ throw error;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Rechargement sécurisé des layouts
614
+ */
615
+ reloadLayouts() {
616
+ this.layoutCache.clear();
617
+ this.layoutSections.clear();
618
+ this.app.logger?.log('reload', 'Layout cache cleared', '🎨 All layouts refreshed');
619
+ return this.app;
620
+ }
621
+
622
+ // === Méthodes utilitaires de sécurité ===
623
+
624
+ /**
625
+ * Nettoyage du nom de section
626
+ */
627
+ sanitizeSectionName(name) {
628
+ if (typeof name !== 'string') return 'default';
629
+ return name.replace(/[^a-zA-Z0-9\-_]/g, '').substring(0, 50) || 'default';
630
+ }
631
+
632
+ /**
633
+ * Nettoyage du contenu de section
634
+ */
635
+ sanitizeSectionContent(content) {
636
+ if (typeof content !== 'string') return '';
637
+ return content.substring(0, this.securityConfig.maxSectionSize);
638
+ }
639
+
640
+ /**
641
+ * Nettoyage des URLs de ressources
642
+ */
643
+ sanitizeResourceUrl(url) {
644
+ if (typeof url !== 'string') return null;
645
+
646
+ // Validation basique d'URL
647
+ if (url.length > 200 || url.includes('<') || url.includes('>')) {
648
+ return null;
649
+ }
650
+
651
+ // Autoriser uniquement les URLs relatives et HTTPS
652
+ if (url.startsWith('/') || url.startsWith('https://') || url.startsWith('./')) {
653
+ return url;
654
+ }
655
+
656
+ return null;
657
+ }
658
+
659
+ /**
660
+ * Nettoyage de texte générique
661
+ */
662
+ sanitizeText(text, maxLength = 100) {
663
+ if (typeof text !== 'string') return '';
664
+ return text.replace(/[<>]/g, '').substring(0, maxLength);
665
+ }
666
+
667
+ /**
668
+ * Échappement HTML
669
+ */
670
+ escapeHtml(text) {
671
+ if (typeof text !== 'string') return '';
672
+
673
+ const escapeMap = {
674
+ '&': '&amp;',
675
+ '<': '&lt;',
676
+ '>': '&gt;',
677
+ '"': '&quot;',
678
+ "'": '&#x27;',
679
+ '/': '&#x2F;'
680
+ };
681
+
682
+ return text.replace(/[&<>"'\/]/g, (char) => escapeMap[char]);
683
+ }
684
+
685
+ /**
686
+ * Nettoyage des paramètres de requête
687
+ */
688
+ sanitizeQueryParams(query) {
689
+ if (!query || typeof query !== 'object') return {};
690
+
691
+ const sanitized = {};
692
+ for (const [key, value] of Object.entries(query)) {
693
+ if (typeof key === 'string' && key.length <= 50) {
694
+ const cleanKey = key.replace(/[^a-zA-Z0-9\-_]/g, '');
695
+ if (typeof value === 'string' && value.length <= 200) {
696
+ sanitized[cleanKey] = value;
697
+ }
698
+ }
699
+ }
700
+
701
+ return sanitized;
702
+ }
703
+
704
+ /**
705
+ * Nettoyage des paramètres de route
706
+ */
707
+ sanitizeParams(params) {
708
+ return this.sanitizeQueryParams(params);
709
+ }
710
+
711
+ /**
712
+ * Fusion sécurisée des options
713
+ */
714
+ mergeOptionsSecurely(baseData, options) {
715
+ const allowedFields = ['title', 'description', 'keywords', 'bodyClass', 'css', 'js'];
716
+ const merged = { ...baseData };
717
+
718
+ allowedFields.forEach(field => {
719
+ if (options[field] !== undefined) {
720
+ if (field === 'css' || field === 'js') {
721
+ if (Array.isArray(options[field])) {
722
+ merged.layout = merged.layout || {};
723
+ merged.layout[field] = options[field].slice(0, 10); // Limiter à 10 ressources
724
+ }
725
+ } else if (typeof options[field] === 'string') {
726
+ merged.meta = merged.meta || {};
727
+ merged.meta[field] = this.sanitizeText(options[field], 200);
728
+ }
729
+ }
730
+ });
731
+
732
+ return merged;
733
+ }
734
+
735
+ /**
736
+ * Nettoyage des options de layout
737
+ */
738
+ sanitizeLayoutOptions(layoutOptions) {
739
+ if (!layoutOptions || typeof layoutOptions !== 'object') {
740
+ return { css: [], js: [], bodyClass: '' };
741
+ }
742
+
743
+ return {
744
+ css: Array.isArray(layoutOptions.css) ? layoutOptions.css.slice(0, 10) : [],
745
+ js: Array.isArray(layoutOptions.js) ? layoutOptions.js.slice(0, 10) : [],
746
+ bodyClass: typeof layoutOptions.bodyClass === 'string'
747
+ ? this.sanitizeText(layoutOptions.bodyClass, 100)
748
+ : ''
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Nettoyage du nom de vue
754
+ */
755
+ sanitizeViewName(view) {
756
+ if (typeof view !== 'string') return 'index';
757
+ return view.replace(/[^a-zA-Z0-9\-_\/]/g, '').substring(0, 100) || 'index';
758
+ }
759
+
760
+ /**
761
+ * Gestion centralisée des erreurs de rendu
762
+ */
763
+ handleRenderError(error, res, view, options, originalRender, callback) {
764
+ this.app.logger?.log('error', 'Layout render error', {
765
+ error: error.message,
766
+ view,
767
+ stack: error.stack
768
+ });
769
+
770
+ // Fallback vers le rendu original
771
+ try {
772
+ originalRender.call(res, view, options, callback);
773
+ } catch (fallbackError) {
774
+ // Dernière tentative avec une réponse d'erreur
775
+ if (res.headersSent) return;
776
+
777
+ res.status(500);
778
+ if (typeof callback === 'function') {
779
+ callback(fallbackError);
780
+ } else {
781
+ res.send('Internal Server Error');
782
+ }
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Initialisation du nettoyage automatique du cache
788
+ */
789
+ initCacheCleanup() {
790
+ // Nettoyage du cache toutes les 30 minutes
791
+ this.cleanupInterval = setInterval(() => {
792
+ this.cleanupCache();
793
+ }, 30 * 60 * 1000);
794
+
795
+ // Nettoyage au démarrage
796
+ process.nextTick(() => this.cleanupCache());
797
+ }
798
+
799
+ /**
800
+ * Nettoyage automatique du cache
801
+ */
802
+ cleanupCache() {
803
+ try {
804
+ // Nettoyage du cache de layouts si trop grand
805
+ if (this.layoutCache.size > this.securityConfig.maxCacheSize) {
806
+ this.layoutCache.clear();
807
+ this.app.logger?.log('maintenance', 'Layout cache cleared', 'Size limit exceeded');
808
+ }
809
+
810
+ // Nettoyage du cache de sections
811
+ if (this.layoutSections.size > this.securityConfig.maxCacheSize) {
812
+ this.layoutSections.clear();
813
+ this.app.logger?.log('maintenance', 'Sections cache cleared', 'Size limit exceeded');
814
+ }
815
+ } catch (error) {
816
+ this.app.logger?.log('error', 'Cache cleanup error', error.message);
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Nettoyage lors de la destruction
822
+ */
823
+ destroy() {
824
+ if (this.cleanupInterval) {
825
+ clearInterval(this.cleanupInterval);
826
+ this.cleanupInterval = null;
827
+ }
828
+
829
+ this.layoutCache.clear();
830
+ this.layoutSections.clear();
831
+ }
832
+ }
833
+
834
+ module.exports = LayoutManager;