mcp-wordpress 1.5.2 → 2.0.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.
- package/README.md +332 -61
- package/dist/cache/CacheInvalidation.d.ts.map +1 -1
- package/dist/cache/CacheInvalidation.js +4 -4
- package/dist/cache/CacheInvalidation.js.map +1 -1
- package/dist/client/MockWordPressClient.d.ts +55 -0
- package/dist/client/MockWordPressClient.d.ts.map +1 -0
- package/dist/client/MockWordPressClient.js +369 -0
- package/dist/client/MockWordPressClient.js.map +1 -0
- package/dist/client/api.d.ts +1 -0
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +26 -60
- package/dist/client/api.js.map +1 -1
- package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
- package/dist/client/managers/AuthenticationManager.js +4 -3
- package/dist/client/managers/AuthenticationManager.js.map +1 -1
- package/dist/config/ConfigurationSchema.d.ts +3 -3
- package/dist/config/ConfigurationSchema.d.ts.map +1 -1
- package/dist/config/ConfigurationSchema.js +7 -24
- package/dist/config/ConfigurationSchema.js.map +1 -1
- package/dist/config/ServerConfiguration.d.ts +8 -0
- package/dist/config/ServerConfiguration.d.ts.map +1 -1
- package/dist/config/ServerConfiguration.js +80 -31
- package/dist/config/ServerConfiguration.js.map +1 -1
- package/dist/docs/DocumentationGenerator.d.ts.map +1 -1
- package/dist/docs/DocumentationGenerator.js +5 -7
- package/dist/docs/DocumentationGenerator.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -29
- package/dist/index.js.map +1 -1
- package/dist/security/InputValidator.d.ts.map +1 -1
- package/dist/security/InputValidator.js +3 -11
- package/dist/security/InputValidator.js.map +1 -1
- package/dist/server/ToolRegistry.d.ts +4 -0
- package/dist/server/ToolRegistry.d.ts.map +1 -1
- package/dist/server/ToolRegistry.js +71 -8
- package/dist/server/ToolRegistry.js.map +1 -1
- package/dist/tools/auth.d.ts.map +1 -1
- package/dist/tools/auth.js +8 -3
- package/dist/tools/auth.js.map +1 -1
- package/dist/tools/posts.d.ts.map +1 -1
- package/dist/tools/posts.js +287 -20
- package/dist/tools/posts.js.map +1 -1
- package/dist/tools/site.d.ts.map +1 -1
- package/dist/tools/site.js +47 -9
- package/dist/tools/site.js.map +1 -1
- package/dist/tools/users.d.ts.map +1 -1
- package/dist/tools/users.js +113 -10
- package/dist/tools/users.js.map +1 -1
- package/dist/utils/enhancedError.d.ts +61 -0
- package/dist/utils/enhancedError.d.ts.map +1 -0
- package/dist/utils/enhancedError.js +221 -0
- package/dist/utils/enhancedError.js.map +1 -0
- package/dist/utils/streaming.d.ts +104 -0
- package/dist/utils/streaming.d.ts.map +1 -0
- package/dist/utils/streaming.js +312 -0
- package/dist/utils/streaming.js.map +1 -0
- package/dist/utils/validation.d.ts +19 -3
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +174 -24
- package/dist/utils/validation.js.map +1 -1
- package/docs/ARCHITECTURE.md +850 -0
- package/docs/CACHING.md +20 -17
- package/docs/CONFIGURATION.md +660 -0
- package/docs/DOCKER.md +61 -60
- package/docs/EVALUATION.md +397 -0
- package/docs/INSTALLATION.md +423 -0
- package/docs/PERFORMANCE_MONITORING.md +17 -15
- package/docs/SECURITY.md +621 -0
- package/docs/SECURITY_TESTING.md +22 -26
- package/docs/TEST_SITE_SETUP.md +136 -0
- package/docs/TROUBLESHOOTING.md +578 -0
- package/docs/api/README.md +76 -91
- package/docs/api/categories/auth.md +0 -2
- package/docs/api/categories/cache.md +0 -2
- package/docs/api/categories/comment.md +0 -2
- package/docs/api/categories/media.md +0 -2
- package/docs/api/categories/page.md +0 -2
- package/docs/api/categories/performance.md +0 -2
- package/docs/api/categories/post.md +0 -2
- package/docs/api/categories/site.md +0 -2
- package/docs/api/categories/taxonomy.md +0 -2
- package/docs/api/categories/user.md +0 -2
- package/docs/api/summary.json +1 -1
- package/docs/api/tools/wp_approve_comment.md +11 -3
- package/docs/api/tools/wp_cache_clear.md +14 -5
- package/docs/api/tools/wp_cache_info.md +14 -5
- package/docs/api/tools/wp_cache_stats.md +14 -5
- package/docs/api/tools/wp_cache_warm.md +14 -5
- package/docs/api/tools/wp_create_application_password.md +11 -3
- package/docs/api/tools/wp_create_category.md +11 -3
- package/docs/api/tools/wp_create_comment.md +14 -5
- package/docs/api/tools/wp_create_page.md +13 -5
- package/docs/api/tools/wp_create_post.md +14 -7
- package/docs/api/tools/wp_create_tag.md +11 -3
- package/docs/api/tools/wp_create_user.md +13 -5
- package/docs/api/tools/wp_delete_application_password.md +11 -3
- package/docs/api/tools/wp_delete_category.md +11 -3
- package/docs/api/tools/wp_delete_comment.md +11 -3
- package/docs/api/tools/wp_delete_media.md +10 -3
- package/docs/api/tools/wp_delete_page.md +10 -3
- package/docs/api/tools/wp_delete_post.md +11 -5
- package/docs/api/tools/wp_delete_tag.md +11 -3
- package/docs/api/tools/wp_delete_user.md +10 -3
- package/docs/api/tools/wp_get_application_passwords.md +11 -3
- package/docs/api/tools/wp_get_auth_status.md +11 -3
- package/docs/api/tools/wp_get_category.md +11 -3
- package/docs/api/tools/wp_get_comment.md +11 -3
- package/docs/api/tools/wp_get_current_user.md +11 -3
- package/docs/api/tools/wp_get_media.md +11 -3
- package/docs/api/tools/wp_get_page.md +11 -3
- package/docs/api/tools/wp_get_page_revisions.md +11 -3
- package/docs/api/tools/wp_get_post.md +12 -5
- package/docs/api/tools/wp_get_post_revisions.md +11 -3
- package/docs/api/tools/wp_get_site_settings.md +10 -3
- package/docs/api/tools/wp_get_tag.md +11 -3
- package/docs/api/tools/wp_get_user.md +11 -3
- package/docs/api/tools/wp_list_categories.md +11 -3
- package/docs/api/tools/wp_list_comments.md +11 -3
- package/docs/api/tools/wp_list_media.md +14 -5
- package/docs/api/tools/wp_list_pages.md +14 -5
- package/docs/api/tools/wp_list_posts.md +15 -7
- package/docs/api/tools/wp_list_tags.md +11 -3
- package/docs/api/tools/wp_list_users.md +11 -3
- package/docs/api/tools/wp_performance_alerts.md +17 -7
- package/docs/api/tools/wp_performance_benchmark.md +17 -7
- package/docs/api/tools/wp_performance_export.md +17 -7
- package/docs/api/tools/wp_performance_history.md +17 -7
- package/docs/api/tools/wp_performance_optimize.md +17 -7
- package/docs/api/tools/wp_performance_stats.md +17 -7
- package/docs/api/tools/wp_search_site.md +11 -3
- package/docs/api/tools/wp_spam_comment.md +11 -3
- package/docs/api/tools/wp_switch_auth_method.md +14 -5
- package/docs/api/tools/wp_test_auth.md +11 -3
- package/docs/api/tools/wp_update_category.md +11 -3
- package/docs/api/tools/wp_update_comment.md +14 -5
- package/docs/api/tools/wp_update_media.md +14 -5
- package/docs/api/tools/wp_update_page.md +13 -5
- package/docs/api/tools/wp_update_post.md +14 -7
- package/docs/api/tools/wp_update_site_settings.md +14 -5
- package/docs/api/tools/wp_update_tag.md +11 -3
- package/docs/api/tools/wp_update_user.md +13 -5
- package/docs/api/tools/wp_upload_media.md +13 -5
- package/docs/api/types/WordPressPost.md +2 -0
- package/docs/code-improvements.md +40 -0
- package/docs/contract-testing.md +1 -1
- package/docs/developer/API_REFERENCE.md +19 -59
- package/docs/developer/ARCHITECTURE.md +8 -11
- package/docs/developer/BUILD_SYSTEM.md +2 -2
- package/docs/developer/CONTRIBUTING.md +3 -5
- package/docs/developer/GITHUB_ACTIONS_SETUP.md +2 -2
- package/docs/developer/MIGRATION_GUIDE.md +5 -6
- package/docs/developer/README.md +2 -1
- package/docs/developer/REFACTORING.md +9 -15
- package/docs/developer/RELEASE_PROCESS.md +4 -3
- package/docs/developer/TESTING.md +2 -2
- package/docs/examples/claude-desktop-config.md +8 -0
- package/docs/integrations/claude-desktop.md +426 -0
- package/docs/integrations/cline.md +537 -0
- package/docs/integrations/vs-code.md +515 -0
- package/docs/releases/COMMUNITY_ANNOUNCEMENT_v1.1.2.md +30 -23
- package/docs/releases/RELEASE_NOTES_v1.1.2.md +7 -6
- package/docs/testing-configurations.md +11 -0
- package/docs/user-guides/DOCKER_NPM_DTX_SETUP.md +3 -2
- package/docs/user-guides/DOCKER_SETUP.md +3 -2
- package/docs/user-guides/DTX_SETUP.md +6 -5
- package/docs/user-guides/DXT_INSTALLATION.md +4 -4
- package/docs/user-guides/NPM_SETUP.md +4 -2
- package/docs/user-guides/NPX_SETUP.md +4 -2
- package/docs/user-guides/SMITHERY_SETUP.md +402 -0
- package/docs/wordpress-rest-api-authentication-troubleshooting.md +45 -42
- package/package.json +12 -2
- package/src/cache/CacheInvalidation.ts +7 -18
- package/src/client/MockWordPressClient.ts +398 -0
- package/src/client/api.ts +77 -237
- package/src/client/managers/AuthenticationManager.ts +19 -56
- package/src/config/ConfigurationSchema.ts +14 -45
- package/src/config/ServerConfiguration.ts +98 -71
- package/src/docs/DocumentationGenerator.ts +39 -123
- package/src/dxt-entry.cjs +4 -1
- package/src/index.ts +35 -54
- package/src/security/InputValidator.ts +15 -57
- package/src/server/ToolRegistry.ts +88 -17
- package/src/tools/auth.ts +15 -22
- package/src/tools/posts.ts +347 -64
- package/src/tools/site.ts +69 -46
- package/src/tools/users.ts +142 -44
- package/src/utils/enhancedError.ts +248 -0
- package/src/utils/streaming.ts +428 -0
- package/src/utils/validation.ts +253 -92
- package/dist/mcp-wordpress-1.5.2.tgz +0 -0
package/src/utils/validation.ts
CHANGED
|
@@ -6,35 +6,61 @@ import { WordPressAPIError } from "../types/client.js";
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Validates and sanitizes numeric IDs
|
|
9
|
+
* Validates and sanitizes numeric IDs with comprehensive edge case handling
|
|
10
10
|
*/
|
|
11
11
|
export function validateId(id: any, fieldName: string = "id"): number {
|
|
12
|
-
|
|
13
|
-
if (
|
|
12
|
+
// Handle null/undefined
|
|
13
|
+
if (id === null || id === undefined) {
|
|
14
|
+
throw new WordPressAPIError(`${fieldName} is required`, 400, "MISSING_PARAMETER");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Convert to string first to handle various input types
|
|
18
|
+
const strId = String(id).trim();
|
|
19
|
+
|
|
20
|
+
// Check for empty string after trim
|
|
21
|
+
if (strId === "") {
|
|
22
|
+
throw new WordPressAPIError(`${fieldName} cannot be empty`, 400, "INVALID_PARAMETER");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle decimal inputs
|
|
26
|
+
if (strId.includes(".")) {
|
|
27
|
+
throw new WordPressAPIError(`${fieldName} must be a whole number, not a decimal`, 400, "INVALID_PARAMETER");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const numId = parseInt(strId, 10);
|
|
31
|
+
|
|
32
|
+
// Check for NaN
|
|
33
|
+
if (isNaN(numId)) {
|
|
34
|
+
throw new WordPressAPIError(`Invalid ${fieldName}: "${id}" is not a valid number`, 400, "INVALID_PARAMETER");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for negative or zero
|
|
38
|
+
if (numId <= 0) {
|
|
14
39
|
throw new WordPressAPIError(
|
|
15
|
-
`Invalid ${fieldName}: must be a positive number`,
|
|
40
|
+
`Invalid ${fieldName}: must be a positive number (got ${numId})`,
|
|
16
41
|
400,
|
|
17
42
|
"INVALID_PARAMETER",
|
|
18
43
|
);
|
|
19
44
|
}
|
|
45
|
+
|
|
46
|
+
// Check for max int32 limit (WordPress database limit)
|
|
47
|
+
if (numId > 2147483647) {
|
|
48
|
+
throw new WordPressAPIError(
|
|
49
|
+
`Invalid ${fieldName}: exceeds maximum allowed value (2147483647)`,
|
|
50
|
+
400,
|
|
51
|
+
"INVALID_PARAMETER",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
return numId;
|
|
21
56
|
}
|
|
22
57
|
|
|
23
58
|
/**
|
|
24
59
|
* Validates string length within bounds
|
|
25
60
|
*/
|
|
26
|
-
export function validateString(
|
|
27
|
-
value: any,
|
|
28
|
-
fieldName: string,
|
|
29
|
-
minLength: number = 1,
|
|
30
|
-
maxLength: number = 1000,
|
|
31
|
-
): string {
|
|
61
|
+
export function validateString(value: any, fieldName: string, minLength: number = 1, maxLength: number = 1000): string {
|
|
32
62
|
if (typeof value !== "string") {
|
|
33
|
-
throw new WordPressAPIError(
|
|
34
|
-
`Invalid ${fieldName}: must be a string`,
|
|
35
|
-
400,
|
|
36
|
-
"INVALID_PARAMETER",
|
|
37
|
-
);
|
|
63
|
+
throw new WordPressAPIError(`Invalid ${fieldName}: must be a string`, 400, "INVALID_PARAMETER");
|
|
38
64
|
}
|
|
39
65
|
|
|
40
66
|
const trimmed = value.trim();
|
|
@@ -52,21 +78,14 @@ export function validateString(
|
|
|
52
78
|
/**
|
|
53
79
|
* Validates and sanitizes file paths to prevent directory traversal
|
|
54
80
|
*/
|
|
55
|
-
export function validateFilePath(
|
|
56
|
-
userPath: string,
|
|
57
|
-
allowedBasePath: string,
|
|
58
|
-
): string {
|
|
81
|
+
export function validateFilePath(userPath: string, allowedBasePath: string): string {
|
|
59
82
|
// Normalize the path to remove ../ and other dangerous patterns
|
|
60
83
|
const normalizedPath = path.normalize(userPath);
|
|
61
84
|
const resolvedPath = path.resolve(allowedBasePath, normalizedPath);
|
|
62
85
|
|
|
63
86
|
// Ensure the resolved path is within the allowed directory
|
|
64
87
|
if (!resolvedPath.startsWith(path.resolve(allowedBasePath))) {
|
|
65
|
-
throw new WordPressAPIError(
|
|
66
|
-
"Invalid file path: access denied",
|
|
67
|
-
403,
|
|
68
|
-
"PATH_TRAVERSAL_ATTEMPT",
|
|
69
|
-
);
|
|
88
|
+
throw new WordPressAPIError("Invalid file path: access denied", 403, "PATH_TRAVERSAL_ATTEMPT");
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
return resolvedPath;
|
|
@@ -76,69 +95,96 @@ export function validateFilePath(
|
|
|
76
95
|
* Validates WordPress post status values
|
|
77
96
|
*/
|
|
78
97
|
export function validatePostStatus(status: string): string {
|
|
79
|
-
const validStatuses = [
|
|
80
|
-
"publish",
|
|
81
|
-
"draft",
|
|
82
|
-
"pending",
|
|
83
|
-
"private",
|
|
84
|
-
"future",
|
|
85
|
-
"auto-draft",
|
|
86
|
-
"trash",
|
|
87
|
-
];
|
|
98
|
+
const validStatuses = ["publish", "draft", "pending", "private", "future", "auto-draft", "trash"];
|
|
88
99
|
if (!validStatuses.includes(status)) {
|
|
89
|
-
throw new WordPressAPIError(
|
|
90
|
-
`Invalid status: must be one of ${validStatuses.join(", ")}`,
|
|
91
|
-
400,
|
|
92
|
-
"INVALID_PARAMETER",
|
|
93
|
-
);
|
|
100
|
+
throw new WordPressAPIError(`Invalid status: must be one of ${validStatuses.join(", ")}`, 400, "INVALID_PARAMETER");
|
|
94
101
|
}
|
|
95
102
|
return status;
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/**
|
|
99
|
-
* Validates and sanitizes URLs
|
|
106
|
+
* Validates and sanitizes URLs with enhanced edge case handling
|
|
100
107
|
*/
|
|
101
108
|
export function validateUrl(url: string, fieldName: string = "url"): string {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// Check for empty or whitespace-only URLs
|
|
110
|
+
const trimmedUrl = url.trim();
|
|
111
|
+
if (!trimmedUrl) {
|
|
112
|
+
throw new WordPressAPIError(`${fieldName} cannot be empty`, 400, "INVALID_PARAMETER");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove trailing slashes for consistency
|
|
116
|
+
const cleanUrl = trimmedUrl.replace(/\/+$/, "");
|
|
117
|
+
|
|
118
|
+
// Check for common URL mistakes
|
|
119
|
+
if (!cleanUrl.match(/^https?:\/\//i)) {
|
|
110
120
|
throw new WordPressAPIError(
|
|
111
|
-
`Invalid ${fieldName}: must
|
|
121
|
+
`Invalid ${fieldName}: must start with http:// or https:// (got "${cleanUrl}")`,
|
|
112
122
|
400,
|
|
113
123
|
"INVALID_PARAMETER",
|
|
114
124
|
);
|
|
115
125
|
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const urlObj = new URL(cleanUrl);
|
|
129
|
+
|
|
130
|
+
// Only allow http and https protocols
|
|
131
|
+
if (!["http:", "https:"].includes(urlObj.protocol)) {
|
|
132
|
+
throw new WordPressAPIError(
|
|
133
|
+
`Invalid ${fieldName}: only HTTP and HTTPS protocols are allowed`,
|
|
134
|
+
400,
|
|
135
|
+
"INVALID_PARAMETER",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate hostname
|
|
140
|
+
if (!urlObj.hostname || urlObj.hostname.length < 3) {
|
|
141
|
+
throw new WordPressAPIError(`Invalid ${fieldName}: hostname is missing or too short`, 400, "INVALID_PARAMETER");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for localhost in production
|
|
145
|
+
if (process.env.NODE_ENV === "production" && (urlObj.hostname === "localhost" || urlObj.hostname === "127.0.0.1")) {
|
|
146
|
+
throw new WordPressAPIError(
|
|
147
|
+
`Invalid ${fieldName}: localhost URLs are not allowed in production`,
|
|
148
|
+
400,
|
|
149
|
+
"INVALID_PARAMETER",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate port if present
|
|
154
|
+
if (urlObj.port) {
|
|
155
|
+
const port = parseInt(urlObj.port);
|
|
156
|
+
if (port < 1 || port > 65535) {
|
|
157
|
+
throw new WordPressAPIError(
|
|
158
|
+
`Invalid ${fieldName}: port number must be between 1 and 65535`,
|
|
159
|
+
400,
|
|
160
|
+
"INVALID_PARAMETER",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return cleanUrl;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof WordPressAPIError) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
throw new WordPressAPIError(`Invalid ${fieldName}: malformed URL "${cleanUrl}"`, 400, "INVALID_PARAMETER");
|
|
171
|
+
}
|
|
116
172
|
}
|
|
117
173
|
|
|
118
174
|
/**
|
|
119
175
|
* Validates file size
|
|
120
176
|
*/
|
|
121
|
-
export function validateFileSize(
|
|
122
|
-
sizeInBytes: number,
|
|
123
|
-
maxSizeInMB: number = 10,
|
|
124
|
-
): void {
|
|
177
|
+
export function validateFileSize(sizeInBytes: number, maxSizeInMB: number = 10): void {
|
|
125
178
|
const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
|
|
126
179
|
if (sizeInBytes > maxSizeInBytes) {
|
|
127
|
-
throw new WordPressAPIError(
|
|
128
|
-
`File size exceeds maximum allowed size of ${maxSizeInMB}MB`,
|
|
129
|
-
413,
|
|
130
|
-
"FILE_TOO_LARGE",
|
|
131
|
-
);
|
|
180
|
+
throw new WordPressAPIError(`File size exceeds maximum allowed size of ${maxSizeInMB}MB`, 413, "FILE_TOO_LARGE");
|
|
132
181
|
}
|
|
133
182
|
}
|
|
134
183
|
|
|
135
184
|
/**
|
|
136
185
|
* Validates MIME types for file uploads
|
|
137
186
|
*/
|
|
138
|
-
export function validateMimeType(
|
|
139
|
-
mimeType: string,
|
|
140
|
-
allowedTypes: string[],
|
|
141
|
-
): void {
|
|
187
|
+
export function validateMimeType(mimeType: string, allowedTypes: string[]): void {
|
|
142
188
|
if (!allowedTypes.includes(mimeType)) {
|
|
143
189
|
throw new WordPressAPIError(
|
|
144
190
|
`Invalid file type: ${mimeType}. Allowed types: ${allowedTypes.join(", ")}`,
|
|
@@ -155,10 +201,7 @@ export function validateMimeType(
|
|
|
155
201
|
*/
|
|
156
202
|
export function sanitizeHtml(html: string): string {
|
|
157
203
|
// Remove script tags and their content
|
|
158
|
-
let sanitized = html.replace(
|
|
159
|
-
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
160
|
-
"",
|
|
161
|
-
);
|
|
204
|
+
let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
162
205
|
|
|
163
206
|
// Remove event handlers
|
|
164
207
|
sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
|
|
@@ -175,18 +218,9 @@ export function sanitizeHtml(html: string): string {
|
|
|
175
218
|
/**
|
|
176
219
|
* Validates array input
|
|
177
220
|
*/
|
|
178
|
-
export function validateArray<T>(
|
|
179
|
-
value: any,
|
|
180
|
-
fieldName: string,
|
|
181
|
-
minItems: number = 0,
|
|
182
|
-
maxItems: number = 100,
|
|
183
|
-
): T[] {
|
|
221
|
+
export function validateArray<T>(value: any, fieldName: string, minItems: number = 0, maxItems: number = 100): T[] {
|
|
184
222
|
if (!Array.isArray(value)) {
|
|
185
|
-
throw new WordPressAPIError(
|
|
186
|
-
`Invalid ${fieldName}: must be an array`,
|
|
187
|
-
400,
|
|
188
|
-
"INVALID_PARAMETER",
|
|
189
|
-
);
|
|
223
|
+
throw new WordPressAPIError(`Invalid ${fieldName}: must be an array`, 400, "INVALID_PARAMETER");
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
if (value.length < minItems || value.length > maxItems) {
|
|
@@ -206,22 +240,24 @@ export function validateArray<T>(
|
|
|
206
240
|
export function validateEmail(email: string): string {
|
|
207
241
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
208
242
|
if (!emailRegex.test(email)) {
|
|
209
|
-
throw new WordPressAPIError(
|
|
210
|
-
"Invalid email address format",
|
|
211
|
-
400,
|
|
212
|
-
"INVALID_PARAMETER",
|
|
213
|
-
);
|
|
243
|
+
throw new WordPressAPIError("Invalid email address format", 400, "INVALID_PARAMETER");
|
|
214
244
|
}
|
|
215
245
|
return email.toLowerCase();
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
/**
|
|
219
|
-
* Validates username format
|
|
249
|
+
* Validates username format with enhanced security checks
|
|
220
250
|
*/
|
|
221
251
|
export function validateUsername(username: string): string {
|
|
252
|
+
// Trim and check for empty
|
|
253
|
+
const trimmed = username.trim();
|
|
254
|
+
if (!trimmed) {
|
|
255
|
+
throw new WordPressAPIError("Username cannot be empty", 400, "INVALID_PARAMETER");
|
|
256
|
+
}
|
|
257
|
+
|
|
222
258
|
// WordPress username rules: alphanumeric, space, underscore, hyphen, period, @ symbol
|
|
223
259
|
const usernameRegex = /^[a-zA-Z0-9 _.\-@]+$/;
|
|
224
|
-
if (!usernameRegex.test(
|
|
260
|
+
if (!usernameRegex.test(trimmed)) {
|
|
225
261
|
throw new WordPressAPIError(
|
|
226
262
|
"Invalid username: can only contain letters, numbers, spaces, and _.-@ symbols",
|
|
227
263
|
400,
|
|
@@ -229,15 +265,27 @@ export function validateUsername(username: string): string {
|
|
|
229
265
|
);
|
|
230
266
|
}
|
|
231
267
|
|
|
232
|
-
|
|
268
|
+
// Length validation
|
|
269
|
+
if (trimmed.length < 3 || trimmed.length > 60) {
|
|
233
270
|
throw new WordPressAPIError(
|
|
234
|
-
|
|
271
|
+
`Invalid username: must be between 3 and 60 characters (got ${trimmed.length})`,
|
|
235
272
|
400,
|
|
236
273
|
"INVALID_PARAMETER",
|
|
237
274
|
);
|
|
238
275
|
}
|
|
239
276
|
|
|
240
|
-
|
|
277
|
+
// Check for consecutive spaces
|
|
278
|
+
if (/\s{2,}/.test(trimmed)) {
|
|
279
|
+
throw new WordPressAPIError("Invalid username: cannot contain consecutive spaces", 400, "INVALID_PARAMETER");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Security: Prevent common problematic usernames
|
|
283
|
+
const blacklist = ["admin", "root", "wordpress", "wp-admin", "administrator"];
|
|
284
|
+
if (blacklist.includes(trimmed.toLowerCase())) {
|
|
285
|
+
throw new WordPressAPIError(`Username "${trimmed}" is reserved and cannot be used`, 400, "RESERVED_USERNAME");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return trimmed;
|
|
241
289
|
}
|
|
242
290
|
|
|
243
291
|
/**
|
|
@@ -245,8 +293,7 @@ export function validateUsername(username: string): string {
|
|
|
245
293
|
* For production, use Redis or similar
|
|
246
294
|
*/
|
|
247
295
|
class RateLimiter {
|
|
248
|
-
private attempts: Map<string, { count: number; resetTime: number }> =
|
|
249
|
-
new Map();
|
|
296
|
+
private attempts: Map<string, { count: number; resetTime: number }> = new Map();
|
|
250
297
|
|
|
251
298
|
constructor(
|
|
252
299
|
private maxAttempts: number = 5,
|
|
@@ -298,13 +345,127 @@ export function validateSearchQuery(query: string): string {
|
|
|
298
345
|
}
|
|
299
346
|
|
|
300
347
|
// Remove SQL-like patterns (basic protection)
|
|
301
|
-
sanitized = sanitized.replace(
|
|
302
|
-
/(\b(union|select|insert|update|delete|drop|create)\b)/gi,
|
|
303
|
-
"",
|
|
304
|
-
);
|
|
348
|
+
sanitized = sanitized.replace(/(\b(union|select|insert|update|delete|drop|create)\b)/gi, "");
|
|
305
349
|
|
|
306
350
|
// Remove special characters that might be used for injection
|
|
307
351
|
sanitized = sanitized.replace(/[<>'"`;\\]/g, "");
|
|
308
352
|
|
|
309
353
|
return sanitized;
|
|
310
354
|
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Validates pagination parameters as a set
|
|
358
|
+
*/
|
|
359
|
+
export function validatePaginationParams(params: { page?: any; per_page?: any; offset?: any }): {
|
|
360
|
+
page?: number;
|
|
361
|
+
per_page?: number;
|
|
362
|
+
offset?: number;
|
|
363
|
+
} {
|
|
364
|
+
const validated: { page?: number; per_page?: number; offset?: number } = {};
|
|
365
|
+
|
|
366
|
+
// Validate page
|
|
367
|
+
if (params.page !== undefined) {
|
|
368
|
+
const page = parseInt(String(params.page), 10);
|
|
369
|
+
if (isNaN(page) || page < 1) {
|
|
370
|
+
throw new WordPressAPIError("Page must be a positive integer", 400, "INVALID_PARAMETER");
|
|
371
|
+
}
|
|
372
|
+
if (page > 10000) {
|
|
373
|
+
throw new WordPressAPIError("Page number too high (max 10000)", 400, "INVALID_PARAMETER");
|
|
374
|
+
}
|
|
375
|
+
validated.page = page;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Validate per_page
|
|
379
|
+
if (params.per_page !== undefined) {
|
|
380
|
+
const perPage = parseInt(String(params.per_page), 10);
|
|
381
|
+
if (isNaN(perPage) || perPage < 1) {
|
|
382
|
+
throw new WordPressAPIError("Per page must be a positive integer", 400, "INVALID_PARAMETER");
|
|
383
|
+
}
|
|
384
|
+
if (perPage > 100) {
|
|
385
|
+
throw new WordPressAPIError(`Per page exceeds maximum allowed (100), got ${perPage}`, 400, "INVALID_PARAMETER");
|
|
386
|
+
}
|
|
387
|
+
validated.per_page = perPage;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Validate offset
|
|
391
|
+
if (params.offset !== undefined) {
|
|
392
|
+
const offset = parseInt(String(params.offset), 10);
|
|
393
|
+
if (isNaN(offset) || offset < 0) {
|
|
394
|
+
throw new WordPressAPIError("Offset must be a non-negative integer", 400, "INVALID_PARAMETER");
|
|
395
|
+
}
|
|
396
|
+
if (offset > 1000000) {
|
|
397
|
+
throw new WordPressAPIError("Offset too large (max 1000000)", 400, "INVALID_PARAMETER");
|
|
398
|
+
}
|
|
399
|
+
validated.offset = offset;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Check for conflicting parameters
|
|
403
|
+
if (validated.page && validated.offset) {
|
|
404
|
+
throw new WordPressAPIError(
|
|
405
|
+
"Cannot use both 'page' and 'offset' parameters together",
|
|
406
|
+
400,
|
|
407
|
+
"CONFLICTING_PARAMETERS",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return validated;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Validates complex post creation parameters
|
|
416
|
+
*/
|
|
417
|
+
export function validatePostParams(params: any): any {
|
|
418
|
+
const validated: any = {};
|
|
419
|
+
|
|
420
|
+
// Title validation
|
|
421
|
+
if (!params.title || typeof params.title !== "string") {
|
|
422
|
+
throw new WordPressAPIError("Post title is required and must be a string", 400, "INVALID_PARAMETER");
|
|
423
|
+
}
|
|
424
|
+
validated.title = validateString(params.title, "title", 1, 200);
|
|
425
|
+
|
|
426
|
+
// Content validation
|
|
427
|
+
if (params.content !== undefined) {
|
|
428
|
+
validated.content = sanitizeHtml(String(params.content));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Status validation with context
|
|
432
|
+
if (params.status) {
|
|
433
|
+
validated.status = validatePostStatus(params.status);
|
|
434
|
+
|
|
435
|
+
// Future posts need a date
|
|
436
|
+
if (validated.status === "future" && !params.date) {
|
|
437
|
+
throw new WordPressAPIError("Future posts require a 'date' parameter", 400, "MISSING_PARAMETER");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Categories and tags validation
|
|
442
|
+
if (params.categories) {
|
|
443
|
+
validated.categories = validateArray(params.categories, "categories", 0, 50);
|
|
444
|
+
validated.categories = validated.categories.map((id: any) => validateId(id, "category ID"));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (params.tags) {
|
|
448
|
+
validated.tags = validateArray(params.tags, "tags", 0, 100);
|
|
449
|
+
validated.tags = validated.tags.map((id: any) => validateId(id, "tag ID"));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Date validation for scheduled posts
|
|
453
|
+
if (params.date) {
|
|
454
|
+
try {
|
|
455
|
+
const date = new Date(params.date);
|
|
456
|
+
if (isNaN(date.getTime())) {
|
|
457
|
+
throw new Error("Invalid date");
|
|
458
|
+
}
|
|
459
|
+
// WordPress expects ISO 8601 format
|
|
460
|
+
validated.date = date.toISOString();
|
|
461
|
+
} catch {
|
|
462
|
+
throw new WordPressAPIError(
|
|
463
|
+
"Invalid date format. Use ISO 8601 format (YYYY-MM-DDTHH:mm:ss)",
|
|
464
|
+
400,
|
|
465
|
+
"INVALID_PARAMETER",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return validated;
|
|
471
|
+
}
|
|
Binary file
|