i18ntk 1.10.1 → 2.0.2
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/LICENSE +1 -1
- package/README.md +141 -1185
- package/main/i18ntk-analyze.js +149 -133
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +4 -4
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +76 -25
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +128 -29
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +10 -396
- package/main/i18ntk-sizing.js +46 -40
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +55 -19
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -30
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +13 -5
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +23 -15
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +23 -20
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +152 -103
- package/utils/config-manager.js +204 -164
- package/utils/config.js +5 -4
- package/utils/env-manager.js +256 -0
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/logger.js +6 -2
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +29 -11
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +462 -248
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/terminal-icons.js +1 -1
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -450
package/utils/security.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
1
|
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Lazy load configManager to avoid circular dependency
|
|
7
|
+
let configManager;
|
|
8
|
+
let configManagerLoadAttempted = false;
|
|
9
|
+
function getConfigManager() {
|
|
10
|
+
if (!configManager && !configManagerLoadAttempted) {
|
|
11
|
+
configManagerLoadAttempted = true;
|
|
12
|
+
try {
|
|
13
|
+
configManager = require('./config-manager');
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// Return null if config-manager can't be loaded
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return configManager;
|
|
20
|
+
}
|
|
5
21
|
|
|
6
22
|
// Lazy load i18n to prevent initialization race conditions
|
|
7
23
|
let i18n;
|
|
@@ -24,17 +40,147 @@ function getI18n() {
|
|
|
24
40
|
* to prevent path traversal, code injection, and other security vulnerabilities
|
|
25
41
|
*/
|
|
26
42
|
class SecurityUtils {
|
|
43
|
+
|
|
44
|
+
// Static properties for operation tracking
|
|
45
|
+
static _operationStack = new Set();
|
|
46
|
+
static _logging = false;
|
|
47
|
+
|
|
48
|
+
constructor() {
|
|
49
|
+
// Instance constructor - static properties are already initialized
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {
|
|
31
|
-
* @
|
|
53
|
+
* Timeout wrapper for synchronous operations to prevent hanging
|
|
54
|
+
* @param {Function} operation - The synchronous operation to wrap
|
|
55
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
56
|
+
* @param {string} operationName - Name of the operation for logging
|
|
57
|
+
* @returns {*} - Operation result or null if timeout/error
|
|
32
58
|
*/
|
|
33
|
-
static
|
|
59
|
+
static withTimeoutSync(operation, timeoutMs = 5000, operationName = 'operation') {
|
|
60
|
+
// Track recursion to prevent infinite loops
|
|
61
|
+
if (!SecurityUtils._operationStack) {
|
|
62
|
+
SecurityUtils._operationStack = new Set();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (SecurityUtils._operationStack.has(operationName)) {
|
|
66
|
+
const i18n = getI18n();
|
|
67
|
+
SecurityUtils.logSecurityEvent(i18n.t('security.recursion_detected', { operation: operationName }), 'error');
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
SecurityUtils._operationStack.add(operationName);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Simple timeout using setTimeout for synchronous operations
|
|
75
|
+
let result = null;
|
|
76
|
+
let hasResult = false;
|
|
77
|
+
let timeoutId = null;
|
|
78
|
+
|
|
79
|
+
timeoutId = setTimeout(() => {
|
|
80
|
+
if (!hasResult) {
|
|
81
|
+
const i18n = getI18n();
|
|
82
|
+
SecurityUtils.logSecurityEvent(i18n.t('security.operation_timeout', { operation: operationName }), 'warning');
|
|
83
|
+
}
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
|
|
86
|
+
// Execute operation synchronously
|
|
87
|
+
result = operation();
|
|
88
|
+
hasResult = true;
|
|
89
|
+
|
|
90
|
+
if (timeoutId) {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const i18n = getI18n();
|
|
97
|
+
console.warn(i18n.t('security.operation_error', { operation: operationName, error: error.message }));
|
|
98
|
+
return null;
|
|
99
|
+
} finally {
|
|
100
|
+
SecurityUtils._operationStack.delete(operationName);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Logs security events for monitoring
|
|
106
|
+
* @param {string} event - Security event description
|
|
107
|
+
* @param {string} level - Log level (info, warn, error)
|
|
108
|
+
* @param {object} details - Additional details
|
|
109
|
+
*/
|
|
110
|
+
static logSecurityEvent(event, level = 'info', details = {}) {
|
|
111
|
+
// Prevent recursive logging which can occur during configuration loading
|
|
112
|
+
if (SecurityUtils._logging) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
SecurityUtils._logging = true;
|
|
117
|
+
try {
|
|
118
|
+
const cfg = getConfigManager()?.getConfig?.() || {};
|
|
119
|
+
const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
|
|
120
|
+
const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
|
|
121
|
+
const currentLevel = envLevel || configLevel || 'warn';
|
|
122
|
+
|
|
123
|
+
const levels = { error: 0, warn: 1, warning: 1, info: 2 };
|
|
124
|
+
const messageLevel = levels[level.toLowerCase()] ?? 2;
|
|
125
|
+
const allowedLevel = levels[currentLevel] ?? 1;
|
|
126
|
+
if (messageLevel > allowedLevel) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const timestamp = new Date().toISOString();
|
|
131
|
+
const logEntry = {
|
|
132
|
+
timestamp,
|
|
133
|
+
level,
|
|
134
|
+
event,
|
|
135
|
+
details: {
|
|
136
|
+
...details,
|
|
137
|
+
pid: process.pid,
|
|
138
|
+
nodeVersion: process.version
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
|
|
143
|
+
if (level === 'error') {
|
|
144
|
+
console.error(message, details);
|
|
145
|
+
} else if (level === 'warn' || level === 'warning') {
|
|
146
|
+
console.warn(message, details);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(message, details);
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
SecurityUtils._logging = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add other static methods here...
|
|
156
|
+
static validatePath(filePath, basePath = process.cwd(), verbose = false) {
|
|
157
|
+
const i18n = getI18n();
|
|
158
|
+
const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
|
|
159
|
+
|
|
34
160
|
try {
|
|
35
161
|
if (!filePath || typeof filePath !== 'string') {
|
|
36
|
-
const
|
|
37
|
-
|
|
162
|
+
const message = useI18n
|
|
163
|
+
? i18n.t('security.pathValidationFailed')
|
|
164
|
+
: 'Path validation failed';
|
|
165
|
+
const reason = useI18n
|
|
166
|
+
? i18n.t('security.invalidInputType')
|
|
167
|
+
: 'Invalid input type';
|
|
168
|
+
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
169
|
+
inputPath: filePath,
|
|
170
|
+
reason
|
|
171
|
+
});
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for obvious dangerous patterns first
|
|
176
|
+
if (!SecurityUtils.isSafePath(filePath)) {
|
|
177
|
+
const message = useI18n
|
|
178
|
+
? i18n.t('security.pathTraversalAttempt')
|
|
179
|
+
: 'Path traversal attempt';
|
|
180
|
+
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
181
|
+
inputPath: filePath,
|
|
182
|
+
reason: 'Contains dangerous patterns'
|
|
183
|
+
});
|
|
38
184
|
return null;
|
|
39
185
|
}
|
|
40
186
|
|
|
@@ -50,65 +196,60 @@ class SecurityUtils {
|
|
|
50
196
|
// If the path doesn't exist yet, fall back to the resolved path
|
|
51
197
|
}
|
|
52
198
|
|
|
53
|
-
//
|
|
199
|
+
// Check for actual path traversal (going outside the base directory)
|
|
54
200
|
const relativePath = path.relative(base, finalPath);
|
|
55
|
-
if (relativePath.startsWith('..')
|
|
56
|
-
const
|
|
57
|
-
|
|
201
|
+
if (relativePath.startsWith('..')) {
|
|
202
|
+
const message = useI18n
|
|
203
|
+
? i18n.t('security.pathTraversalAttempt')
|
|
204
|
+
: 'Path traversal attempt';
|
|
205
|
+
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
206
|
+
inputPath: filePath,
|
|
207
|
+
resolvedPath: finalPath,
|
|
208
|
+
basePath: base,
|
|
209
|
+
relativePath: relativePath
|
|
210
|
+
});
|
|
58
211
|
return null;
|
|
59
212
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
213
|
+
|
|
214
|
+
// Allow absolute paths that resolve within the project structure
|
|
215
|
+
// The isSafePath check above already filtered out dangerous absolute paths
|
|
216
|
+
|
|
217
|
+
if (verbose) {
|
|
218
|
+
const successMsg = useI18n
|
|
219
|
+
? i18n.t('security.pathValidated')
|
|
220
|
+
: 'Path validated';
|
|
221
|
+
SecurityUtils.logSecurityEvent(successMsg, 'info', {
|
|
222
|
+
inputPath: filePath,
|
|
223
|
+
resolvedPath: finalPath
|
|
224
|
+
});
|
|
225
|
+
}
|
|
64
226
|
return finalPath;
|
|
65
227
|
} catch (error) {
|
|
66
|
-
const
|
|
67
|
-
|
|
228
|
+
const message = useI18n
|
|
229
|
+
? i18n.t('security.pathValidationError')
|
|
230
|
+
: 'Path validation error';
|
|
231
|
+
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
232
|
+
inputPath: filePath,
|
|
233
|
+
error: error.message
|
|
234
|
+
});
|
|
68
235
|
return null;
|
|
69
236
|
}
|
|
70
237
|
}
|
|
71
238
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* @returns {Promise<string|null>} - File content or null if error
|
|
78
|
-
*/
|
|
79
|
-
static async safeReadFile(filePath, basePath, encoding = 'utf8') {
|
|
80
|
-
const validatedPath = this.validatePath(filePath, basePath);
|
|
81
|
-
if (!validatedPath) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
// Check if file exists and is readable
|
|
87
|
-
await fs.promises.access(validatedPath, fs.constants.R_OK);
|
|
88
|
-
|
|
89
|
-
// Read file with size limit (10MB max)
|
|
90
|
-
const stats = await fs.promises.stat(validatedPath);
|
|
91
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
92
|
-
const i18n = getI18n();
|
|
93
|
-
console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
|
|
94
|
-
return null;
|
|
239
|
+
static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
|
|
240
|
+
return this.withTimeoutSync(() => {
|
|
241
|
+
const validatedPath = this.validatePath(filePath, basePath);
|
|
242
|
+
if (!validatedPath) {
|
|
243
|
+
return false;
|
|
95
244
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
245
|
+
try {
|
|
246
|
+
return fs.existsSync(validatedPath);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}, timeoutMs, 'safeExistsSync');
|
|
103
251
|
}
|
|
104
252
|
|
|
105
|
-
/**
|
|
106
|
-
* Safely reads a file synchronously with path validation and error handling
|
|
107
|
-
* @param {string} filePath - Path to the file
|
|
108
|
-
* @param {string} basePath - Base path for validation
|
|
109
|
-
* @param {string} encoding - File encoding (default: 'utf8')
|
|
110
|
-
* @returns {string|null} - File content or null if error
|
|
111
|
-
*/
|
|
112
253
|
static safeReadFileSync(filePath, basePath, encoding = 'utf8') {
|
|
113
254
|
const validatedPath = this.validatePath(filePath, basePath);
|
|
114
255
|
if (!validatedPath) {
|
|
@@ -118,14 +259,14 @@ class SecurityUtils {
|
|
|
118
259
|
try {
|
|
119
260
|
// Check if file exists and is readable
|
|
120
261
|
fs.accessSync(validatedPath, fs.constants.R_OK);
|
|
121
|
-
|
|
262
|
+
|
|
122
263
|
// Read file with size limit (10MB max)
|
|
123
264
|
const stats = fs.statSync(validatedPath);
|
|
124
265
|
if (stats.size > 10 * 1024 * 1024) {
|
|
125
266
|
console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
|
|
126
267
|
return null;
|
|
127
268
|
}
|
|
128
|
-
|
|
269
|
+
|
|
129
270
|
return fs.readFileSync(validatedPath, encoding);
|
|
130
271
|
} catch (error) {
|
|
131
272
|
console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
|
|
@@ -133,34 +274,34 @@ class SecurityUtils {
|
|
|
133
274
|
}
|
|
134
275
|
}
|
|
135
276
|
|
|
136
|
-
|
|
137
|
-
* Safely writes a file with path validation and error handling
|
|
138
|
-
* @param {string} filePath - Path to the file
|
|
139
|
-
* @param {string} content - Content to write
|
|
140
|
-
* @param {string} basePath - Base path for validation
|
|
141
|
-
* @param {string} encoding - File encoding (default: 'utf8')
|
|
142
|
-
* @returns {Promise<boolean>} - Success status
|
|
143
|
-
*/
|
|
144
|
-
static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') {
|
|
277
|
+
static safeWriteFileSync(filePath, content, basePath, encoding = 'utf8') {
|
|
145
278
|
const validatedPath = this.validatePath(filePath, basePath);
|
|
146
279
|
if (!validatedPath) {
|
|
147
280
|
return false;
|
|
148
281
|
}
|
|
149
282
|
|
|
150
283
|
try {
|
|
284
|
+
// Validate content is a string or Buffer
|
|
285
|
+
if (typeof content !== 'string' && !Buffer.isBuffer(content)) {
|
|
286
|
+
const i18n = getI18n();
|
|
287
|
+
console.warn(i18n.t('security.file_write_error', { errorMessage: 'Content must be a string or Buffer' }));
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
151
291
|
// Validate content size (10MB max)
|
|
152
|
-
|
|
292
|
+
const contentSize = typeof content === 'string' ? content.length : content.length;
|
|
293
|
+
if (contentSize > 10 * 1024 * 1024) {
|
|
153
294
|
const i18n = getI18n();
|
|
154
295
|
console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
|
|
155
296
|
return false;
|
|
156
297
|
}
|
|
157
|
-
|
|
298
|
+
|
|
158
299
|
// Ensure directory exists
|
|
159
300
|
const dir = path.dirname(validatedPath);
|
|
160
|
-
|
|
161
|
-
|
|
301
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
302
|
+
|
|
162
303
|
// Write file with proper permissions
|
|
163
|
-
|
|
304
|
+
fs.writeFileSync(validatedPath, content, { encoding, mode: 0o644 });
|
|
164
305
|
return true;
|
|
165
306
|
} catch (error) {
|
|
166
307
|
const i18n = getI18n();
|
|
@@ -170,37 +311,99 @@ class SecurityUtils {
|
|
|
170
311
|
}
|
|
171
312
|
|
|
172
313
|
/**
|
|
173
|
-
*
|
|
174
|
-
* @param {string}
|
|
175
|
-
* @param {
|
|
176
|
-
* @
|
|
314
|
+
* Async compatibility wrapper for safeReadFileSync.
|
|
315
|
+
* @param {string} filePath
|
|
316
|
+
* @param {string} basePath
|
|
317
|
+
* @param {string} encoding
|
|
318
|
+
* @returns {Promise<string|null>}
|
|
177
319
|
*/
|
|
178
|
-
static
|
|
179
|
-
|
|
320
|
+
static async safeReadFile(filePath, basePath, encoding = 'utf8') {
|
|
321
|
+
return this.safeReadFileSync(filePath, basePath, encoding);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Async compatibility wrapper for safeWriteFileSync.
|
|
326
|
+
* @param {string} filePath
|
|
327
|
+
* @param {string|Buffer} content
|
|
328
|
+
* @param {string} basePath
|
|
329
|
+
* @param {string} encoding
|
|
330
|
+
* @returns {Promise<boolean>}
|
|
331
|
+
*/
|
|
332
|
+
static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') {
|
|
333
|
+
return this.safeWriteFileSync(filePath, content, basePath, encoding);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
static safeStatSync(filePath, basePath) {
|
|
337
|
+
const validatedPath = this.validatePath(filePath, basePath);
|
|
338
|
+
if (!validatedPath) {
|
|
180
339
|
return null;
|
|
181
340
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
console.warn(i18n.t('security.json_string_too_large'));
|
|
341
|
+
try {
|
|
342
|
+
return fs.statSync(validatedPath);
|
|
343
|
+
} catch {
|
|
186
344
|
return null;
|
|
187
345
|
}
|
|
346
|
+
}
|
|
188
347
|
|
|
348
|
+
static safeReaddirSync(dirPath, basePath, options) {
|
|
349
|
+
const validatedPath = this.validatePath(dirPath, basePath);
|
|
350
|
+
if (!validatedPath) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
189
353
|
try {
|
|
190
|
-
return
|
|
191
|
-
} catch
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
354
|
+
return fs.readdirSync(validatedPath, options);
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
static safeMkdirSync(dirPath, basePath, options = { recursive: true }) {
|
|
361
|
+
const validatedPath = this.validatePath(dirPath, basePath);
|
|
362
|
+
if (!validatedPath) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
fs.mkdirSync(validatedPath, options);
|
|
367
|
+
return true;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
195
370
|
}
|
|
196
371
|
}
|
|
197
372
|
|
|
198
373
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* @param {object}
|
|
202
|
-
* @
|
|
374
|
+
* Safely parse JSON content.
|
|
375
|
+
* Accepts both raw JSON strings and already-parsed objects.
|
|
376
|
+
* @param {string|object} input - JSON string or object
|
|
377
|
+
* @param {*} fallback - Value to return on parse error
|
|
378
|
+
* @returns {*}
|
|
203
379
|
*/
|
|
380
|
+
static safeParseJSON(input, fallback = null) {
|
|
381
|
+
if (input === null || input === undefined) {
|
|
382
|
+
return fallback;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (typeof input === 'object') {
|
|
386
|
+
return input;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (typeof input !== 'string') {
|
|
390
|
+
return fallback;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const trimmed = input.trim();
|
|
394
|
+
if (!trimmed) {
|
|
395
|
+
return fallback;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const normalized = trimmed.charCodeAt(0) === 0xFEFF ? trimmed.slice(1) : trimmed;
|
|
400
|
+
return JSON.parse(normalized);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn(`Invalid JSON content: ${error.message}`);
|
|
403
|
+
return fallback;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
204
407
|
static sanitizeInput(input, options = {}) {
|
|
205
408
|
if (!input || typeof input !== 'string') {
|
|
206
409
|
return '';
|
|
@@ -239,7 +442,7 @@ class SecurityUtils {
|
|
|
239
442
|
const isFilePath = sanitized.includes('/') || sanitized.includes('\\') || sanitized.includes('.');
|
|
240
443
|
const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
|
|
241
444
|
if (!isFilePath && !isCommonContent) {
|
|
242
|
-
|
|
445
|
+
const i18n = getI18n();
|
|
243
446
|
console.warn(i18n.t('security.inputDisallowedCharacters'));
|
|
244
447
|
}
|
|
245
448
|
// Allow more characters for file paths and content
|
|
@@ -249,143 +452,37 @@ class SecurityUtils {
|
|
|
249
452
|
return sanitized;
|
|
250
453
|
}
|
|
251
454
|
|
|
252
|
-
/**
|
|
253
|
-
* Validates command line arguments
|
|
254
|
-
* @param {object} args - Command line arguments
|
|
255
|
-
* @returns {object} - Validated arguments
|
|
256
|
-
*/
|
|
257
|
-
static async validateCommandArgs(args) {
|
|
258
|
-
const i18n = getI18n();
|
|
259
|
-
const validatedArgs = {};
|
|
260
|
-
const allowedArgs = [
|
|
261
|
-
'source-dir', 'i18n-dir', 'output-dir', 'output-report',
|
|
262
|
-
'help', 'language', 'strict-mode', 'exclude-files', 'no-prompt'
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
for (const [key, value] of Object.entries(args)) {
|
|
266
|
-
if (allowedArgs.includes(key)) {
|
|
267
|
-
validatedArgs[key] = value;
|
|
268
|
-
} else {
|
|
269
|
-
console.warn(i18n.t('security.unknown_command_argument', { key }));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return validatedArgs;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Validates configuration object
|
|
278
|
-
* @param {object} config - Configuration object
|
|
279
|
-
* @returns {object|null} - Validated configuration or null if invalid
|
|
280
|
-
*/
|
|
281
|
-
static validateConfig(config) {
|
|
282
|
-
const i18n = getI18n();
|
|
283
|
-
if (!config || typeof config !== 'object') {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const validatedConfig = {};
|
|
288
|
-
const allowedKeys = [
|
|
289
|
-
'version', 'sourceDir', 'outputDir', 'defaultLanguage', 'supportedLanguages',
|
|
290
|
-
'filePattern', 'excludePatterns', 'reportFormat', 'logLevel',
|
|
291
|
-
'i18nDir', 'sourceLanguage', 'excludeDirs', 'includeExtensions',
|
|
292
|
-
'translationPatterns', 'notTranslatedMarker', 'excludeFiles', 'strictMode',
|
|
293
|
-
'uiLanguage', 'language', 'sizeLimit', 'defaultLanguages', 'reportLanguage',
|
|
294
|
-
'theme', 'autoSave', 'notifications', 'dateFormat', 'timeFormat', 'timezone',
|
|
295
|
-
'processing', 'performance', 'advanced', 'security', 'debug', 'projectRoot', 'scriptDirectories',
|
|
296
|
-
'supportedExtensions', 'settings', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
|
|
297
|
-
'displayPaths', 'reports', 'ui', 'behavior', 'dateTime', 'backup', 'framework',
|
|
298
|
-
'notTranslatedMarkers', 'placeholderStyles'
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
const strict = config.security?.strictConfig || false;
|
|
302
|
-
|
|
303
|
-
for (const [key, value] of Object.entries(config)) {
|
|
304
|
-
if (!allowedKeys.includes(key)) {
|
|
305
|
-
if (strict) {
|
|
306
|
-
console.warn(i18n.t('security.unknown_config_key', { key }));
|
|
307
|
-
}
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Validate specific config values
|
|
312
|
-
switch (key) {
|
|
313
|
-
case 'sourceDir':
|
|
314
|
-
case 'outputDir':
|
|
315
|
-
if (typeof value === 'string') {
|
|
316
|
-
// Basic path validation - will be further validated when used
|
|
317
|
-
validatedConfig[key] = this.sanitizeInput(value, {
|
|
318
|
-
allowedChars: /^[a-zA-Z0-9\-_\.\,\/\\\:\s]+$/,
|
|
319
|
-
maxLength: 500
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
break;
|
|
323
|
-
case 'supportedLanguages':
|
|
324
|
-
if (Array.isArray(value)) {
|
|
325
|
-
validatedConfig[key] = value.filter(lang =>
|
|
326
|
-
typeof lang === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(lang)
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
break;
|
|
330
|
-
case 'defaultLanguage':
|
|
331
|
-
if (typeof value === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(value)) {
|
|
332
|
-
validatedConfig[key] = value;
|
|
333
|
-
}
|
|
334
|
-
break;
|
|
335
|
-
default:
|
|
336
|
-
if (typeof value === 'string') {
|
|
337
|
-
validatedConfig[key] = this.sanitizeInput(value);
|
|
338
|
-
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
|
339
|
-
validatedConfig[key] = value;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return validatedConfig;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Generates a secure hash for file integrity checking
|
|
349
|
-
* @param {string} content - Content to hash
|
|
350
|
-
* @returns {string} - SHA-256 hash
|
|
351
|
-
*/
|
|
352
455
|
static generateHash(content) {
|
|
353
456
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
354
457
|
}
|
|
355
458
|
|
|
356
|
-
/**
|
|
357
|
-
* Securely saves an encrypted PIN to the settings directory
|
|
358
|
-
* @param {string} pin - 4 digit PIN
|
|
359
|
-
* @returns {Promise<boolean>} - success status
|
|
360
|
-
*/
|
|
361
|
-
static async saveEncryptedPin(pin) {
|
|
362
|
-
try {
|
|
363
|
-
const hash = crypto.createHash('sha256').update(pin).digest('hex');
|
|
364
|
-
const settingsDir = require('../settings/settings-manager').configDir;
|
|
365
|
-
const pinFile = path.join(settingsDir, 'admin-pin.hash');
|
|
366
|
-
await fs.promises.mkdir(settingsDir, { recursive: true });
|
|
367
|
-
await fs.promises.writeFile(pinFile, hash, 'utf8');
|
|
368
|
-
return true;
|
|
369
|
-
} catch (error) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Checks if a file path is safe for operations
|
|
376
|
-
* @param {string} filePath - File path to check
|
|
377
|
-
* @returns {boolean} - Whether the path is safe
|
|
378
|
-
*/
|
|
379
459
|
static isSafePath(filePath) {
|
|
380
460
|
if (!filePath || typeof filePath !== 'string') {
|
|
381
461
|
return false;
|
|
382
462
|
}
|
|
383
463
|
|
|
384
|
-
//
|
|
464
|
+
// Allow legitimate Windows drive letter paths
|
|
465
|
+
if (filePath.match(/^[A-Z]:[\/\\]/)) {
|
|
466
|
+
const afterDrive = filePath.substring(3);
|
|
467
|
+
// Only check the part after the drive letter for dangerous patterns
|
|
468
|
+
const dangerousPatterns = [
|
|
469
|
+
/\.\./, // Parent directory traversal
|
|
470
|
+
/~/, // Home directory
|
|
471
|
+
/\$\{/, // Variable expansion
|
|
472
|
+
/`/, // Command substitution
|
|
473
|
+
/\|/, // Pipe
|
|
474
|
+
/;/, // Command separator
|
|
475
|
+
/&/, // Background process
|
|
476
|
+
/>/, // Redirect
|
|
477
|
+
/</ // Redirect
|
|
478
|
+
];
|
|
479
|
+
return !dangerousPatterns.some(pattern => pattern.test(afterDrive));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check for dangerous patterns in non-Windows paths
|
|
385
483
|
const dangerousPatterns = [
|
|
386
484
|
/\.\./, // Parent directory traversal
|
|
387
|
-
/^\//, // Absolute path (Unix)
|
|
388
|
-
/^[A-Z]:\\/, // Absolute path (Windows)
|
|
485
|
+
/^\//, // Absolute path (Unix) - but allow for legitimate use
|
|
389
486
|
/~/, // Home directory
|
|
390
487
|
/\$\{/, // Variable expansion
|
|
391
488
|
/`/, // Command substitution
|
|
@@ -396,43 +493,160 @@ class SecurityUtils {
|
|
|
396
493
|
/</ // Redirect
|
|
397
494
|
];
|
|
398
495
|
|
|
496
|
+
// Allow absolute paths that are within the project structure
|
|
497
|
+
if (filePath.startsWith('/') || filePath.startsWith('\\')) {
|
|
498
|
+
// Allow absolute paths but check for dangerous patterns
|
|
499
|
+
const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
|
|
500
|
+
return !hasDangerousPatterns;
|
|
501
|
+
}
|
|
502
|
+
|
|
399
503
|
return !dangerousPatterns.some(pattern => pattern.test(filePath));
|
|
400
504
|
}
|
|
505
|
+
static validateConfig(config) {
|
|
506
|
+
if (!config || typeof config !== 'object') {
|
|
507
|
+
SecurityUtils.logSecurityEvent('Invalid configuration object provided', 'error', {
|
|
508
|
+
configType: typeof config
|
|
509
|
+
});
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
401
512
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
* @param {string} event - Security event description
|
|
405
|
-
* @param {string} level - Log level (info, warn, error)
|
|
406
|
-
* @param {object} details - Additional details
|
|
407
|
-
*/
|
|
408
|
-
static logSecurityEvent(event, level = 'info', details = {}) {
|
|
409
|
-
const timestamp = new Date().toISOString();
|
|
410
|
-
const logEntry = {
|
|
411
|
-
timestamp,
|
|
412
|
-
level,
|
|
413
|
-
event,
|
|
414
|
-
details: {
|
|
415
|
-
...details,
|
|
416
|
-
pid: process.pid,
|
|
417
|
-
nodeVersion: process.version
|
|
418
|
-
}
|
|
419
|
-
};
|
|
513
|
+
const sanitized = { ...config };
|
|
514
|
+
const i18n = getI18n();
|
|
420
515
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
516
|
+
// Define allowed configuration properties
|
|
517
|
+
const allowedProperties = new Set([
|
|
518
|
+
// Core directories and paths
|
|
519
|
+
'projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
|
|
520
|
+
// Language settings
|
|
521
|
+
'sourceLanguage', 'uiLanguage', 'language', 'defaultLanguages', 'supportedLanguages',
|
|
522
|
+
// Translation markers and content
|
|
523
|
+
'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
|
|
524
|
+
// File handling
|
|
525
|
+
'supportedExtensions', 'excludeFiles', 'excludeDirs', 'includeFiles', 'includeDirs',
|
|
526
|
+
// Operational settings
|
|
527
|
+
'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories',
|
|
528
|
+
// Framework and processing
|
|
529
|
+
'framework', 'processing', 'performance', 'advanced',
|
|
530
|
+
// UI and theme settings
|
|
531
|
+
'theme', 'ui', 'setup', 'reports', 'display', 'interface',
|
|
532
|
+
// Security and settings
|
|
533
|
+
'security', 'settings', 'preferences', 'config', 'configuration',
|
|
534
|
+
// Additional common properties
|
|
535
|
+
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
536
|
+
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled'
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
// Remove unknown properties
|
|
540
|
+
Object.keys(sanitized).forEach(key => {
|
|
541
|
+
if (!allowedProperties.has(key)) {
|
|
542
|
+
// Only log warnings for properties that might be security risks
|
|
543
|
+
const value = sanitized[key];
|
|
544
|
+
const isSuspicious = typeof value === 'string' &&
|
|
545
|
+
(value.includes('..') || value.includes('/') || value.includes('\\') ||
|
|
546
|
+
value.includes('$') || value.includes('`') || value.includes('|') ||
|
|
547
|
+
value.includes(';') || value.includes('&'));
|
|
548
|
+
|
|
549
|
+
if (isSuspicious) {
|
|
550
|
+
SecurityUtils.logSecurityEvent('Removing potentially suspicious configuration property', 'warn', {
|
|
551
|
+
property: key,
|
|
552
|
+
value: sanitized[key]
|
|
553
|
+
});
|
|
554
|
+
} else {
|
|
555
|
+
// Use info level for normal unknown properties to reduce noise
|
|
556
|
+
SecurityUtils.logSecurityEvent('Removing unknown configuration property', 'info', {
|
|
557
|
+
property: key,
|
|
558
|
+
value: sanitized[key]
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
delete sanitized[key];
|
|
426
562
|
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Validate and sanitize path properties
|
|
566
|
+
const pathProperties = ['projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir'];
|
|
567
|
+
|
|
568
|
+
pathProperties.forEach(prop => {
|
|
569
|
+
if (sanitized[prop] && typeof sanitized[prop] === 'string') {
|
|
570
|
+
let originalPath = sanitized[prop];
|
|
571
|
+
|
|
572
|
+
// Skip validation for legitimate absolute paths
|
|
573
|
+
const isWindowsAbsolute = originalPath.match(/^[A-Z]:[\/\\]/i);
|
|
574
|
+
const isUnixAbsolute = originalPath.startsWith('/') || originalPath.startsWith('\\');
|
|
575
|
+
|
|
576
|
+
if (isWindowsAbsolute || isUnixAbsolute) {
|
|
577
|
+
// Allow absolute paths - they're legitimate for project directories
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Only validate relative paths for dangerous patterns
|
|
582
|
+
if (!SecurityUtils.isSafePath(originalPath)) {
|
|
583
|
+
SecurityUtils.logSecurityEvent('Path validation failed for configuration property', 'warn', {
|
|
584
|
+
property: prop,
|
|
585
|
+
originalPath: originalPath
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// For relative paths, ensure they're safe
|
|
589
|
+
let sanitizedPath = originalPath.replace(/\.\.[\/\\]/g, '');
|
|
590
|
+
sanitizedPath = sanitizedPath.replace(/[|;&$`{}()[\]<>?]/g, '');
|
|
591
|
+
|
|
592
|
+
if (sanitizedPath !== originalPath) {
|
|
593
|
+
SecurityUtils.logSecurityEvent('Path sanitized for configuration property', 'info', {
|
|
594
|
+
property: prop,
|
|
595
|
+
originalPath: originalPath,
|
|
596
|
+
sanitizedPath: sanitizedPath
|
|
597
|
+
});
|
|
598
|
+
sanitized[prop] = sanitizedPath;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
433
601
|
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Validate security settings
|
|
605
|
+
if (sanitized.security) {
|
|
606
|
+
const security = sanitized.security;
|
|
607
|
+
|
|
608
|
+
// Validate session timeout (should be reasonable)
|
|
609
|
+
if (security.sessionTimeout && (typeof security.sessionTimeout !== 'number' || security.sessionTimeout < 60000 || security.sessionTimeout > 86400000)) {
|
|
610
|
+
SecurityUtils.logSecurityEvent('Invalid session timeout in security configuration', 'warn', {
|
|
611
|
+
sessionTimeout: security.sessionTimeout
|
|
612
|
+
});
|
|
613
|
+
security.sessionTimeout = 1800000; // Default to 30 minutes
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Validate max failed attempts
|
|
617
|
+
if (security.maxFailedAttempts && (typeof security.maxFailedAttempts !== 'number' || security.maxFailedAttempts < 1 || security.maxFailedAttempts > 10)) {
|
|
618
|
+
SecurityUtils.logSecurityEvent('Invalid max failed attempts in security configuration', 'warn', {
|
|
619
|
+
maxFailedAttempts: security.maxFailedAttempts
|
|
620
|
+
});
|
|
621
|
+
security.maxFailedAttempts = 3; // Default to 3 attempts
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Validate language settings
|
|
626
|
+
if (sanitized.sourceLanguage && typeof sanitized.sourceLanguage === 'string') {
|
|
627
|
+
// Sanitize language code (only allow alphanumeric, hyphens, underscores)
|
|
628
|
+
sanitized.sourceLanguage = sanitized.sourceLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
434
629
|
}
|
|
630
|
+
|
|
631
|
+
if (sanitized.uiLanguage && typeof sanitized.uiLanguage === 'string') {
|
|
632
|
+
sanitized.uiLanguage = sanitized.uiLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Validate default languages array
|
|
636
|
+
if (sanitized.defaultLanguages && Array.isArray(sanitized.defaultLanguages)) {
|
|
637
|
+
sanitized.defaultLanguages = sanitized.defaultLanguages
|
|
638
|
+
.filter(lang => typeof lang === 'string')
|
|
639
|
+
.map(lang => lang.replace(/[^a-zA-Z0-9\-_]/g, ''))
|
|
640
|
+
.filter(lang => lang.length > 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
SecurityUtils.logSecurityEvent('Configuration validation completed', 'info', {
|
|
644
|
+
propertiesCount: Object.keys(sanitized).length,
|
|
645
|
+
sanitizedPaths: pathProperties.filter(prop => sanitized[prop]).length
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return sanitized;
|
|
435
649
|
}
|
|
436
650
|
}
|
|
437
651
|
|
|
438
|
-
module.exports = SecurityUtils;
|
|
652
|
+
module.exports = SecurityUtils;
|