mcp-wordpress 2.4.2 → 2.5.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.
Files changed (348) hide show
  1. package/README.md +114 -48
  2. package/dist/ajv-patch.js +34 -0
  3. package/dist/cache/CacheInvalidation.d.ts +3 -1
  4. package/dist/cache/CacheInvalidation.d.ts.map +1 -1
  5. package/dist/cache/CacheInvalidation.js +10 -4
  6. package/dist/cache/CacheInvalidation.js.map +1 -1
  7. package/dist/cache/CacheManager.d.ts +3 -2
  8. package/dist/cache/CacheManager.d.ts.map +1 -1
  9. package/dist/cache/CacheManager.js +11 -3
  10. package/dist/cache/CacheManager.js.map +1 -1
  11. package/dist/cache/HttpCacheWrapper.d.ts +7 -6
  12. package/dist/cache/HttpCacheWrapper.d.ts.map +1 -1
  13. package/dist/cache/HttpCacheWrapper.js +8 -5
  14. package/dist/cache/HttpCacheWrapper.js.map +1 -1
  15. package/dist/cache/__tests__/HttpCacheWrapper.test.js +6 -5
  16. package/dist/cache/__tests__/HttpCacheWrapper.test.js.map +1 -1
  17. package/dist/cache/index.d.ts +3 -3
  18. package/dist/cache/index.d.ts.map +1 -1
  19. package/dist/cache/index.js +1 -1
  20. package/dist/cache/index.js.map +1 -1
  21. package/dist/client/CachedWordPressClient.d.ts +23 -9
  22. package/dist/client/CachedWordPressClient.d.ts.map +1 -1
  23. package/dist/client/CachedWordPressClient.js +4 -1
  24. package/dist/client/CachedWordPressClient.js.map +1 -1
  25. package/dist/client/MockWordPressClient.d.ts +2 -1
  26. package/dist/client/MockWordPressClient.d.ts.map +1 -1
  27. package/dist/client/MockWordPressClient.js +3 -1
  28. package/dist/client/MockWordPressClient.js.map +1 -1
  29. package/dist/client/api.d.ts +17 -13
  30. package/dist/client/api.d.ts.map +1 -1
  31. package/dist/client/api.js +135 -30
  32. package/dist/client/api.js.map +1 -1
  33. package/dist/client/auth.d.ts.map +1 -1
  34. package/dist/client/auth.js +2 -3
  35. package/dist/client/auth.js.map +1 -1
  36. package/dist/client/managers/AuthenticationManager.d.ts +55 -2
  37. package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
  38. package/dist/client/managers/AuthenticationManager.js +269 -71
  39. package/dist/client/managers/AuthenticationManager.js.map +1 -1
  40. package/dist/client/managers/BaseManager.d.ts +3 -3
  41. package/dist/client/managers/BaseManager.d.ts.map +1 -1
  42. package/dist/client/managers/BaseManager.js +11 -5
  43. package/dist/client/managers/BaseManager.js.map +1 -1
  44. package/dist/client/managers/RequestManager.d.ts +2 -2
  45. package/dist/client/managers/RequestManager.d.ts.map +1 -1
  46. package/dist/client/managers/RequestManager.js +25 -12
  47. package/dist/client/managers/RequestManager.js.map +1 -1
  48. package/dist/config/Config.d.ts +155 -0
  49. package/dist/config/Config.d.ts.map +1 -0
  50. package/dist/config/Config.js +215 -0
  51. package/dist/config/Config.js.map +1 -0
  52. package/dist/config/ConfigurationSchema.d.ts +21 -21
  53. package/dist/config/ConfigurationSchema.d.ts.map +1 -1
  54. package/dist/config/ConfigurationSchema.js +19 -2
  55. package/dist/config/ConfigurationSchema.js.map +1 -1
  56. package/dist/config/ServerConfiguration.d.ts +2 -1
  57. package/dist/config/ServerConfiguration.d.ts.map +1 -1
  58. package/dist/config/ServerConfiguration.js +50 -41
  59. package/dist/config/ServerConfiguration.js.map +1 -1
  60. package/dist/docs/DocumentationGenerator.d.ts +9 -8
  61. package/dist/docs/DocumentationGenerator.d.ts.map +1 -1
  62. package/dist/docs/DocumentationGenerator.js +10 -7
  63. package/dist/docs/DocumentationGenerator.js.map +1 -1
  64. package/dist/docs/MarkdownFormatter.d.ts.map +1 -1
  65. package/dist/docs/MarkdownFormatter.js +3 -2
  66. package/dist/docs/MarkdownFormatter.js.map +1 -1
  67. package/dist/dxt-entry.cjs +81 -0
  68. package/dist/dxt-entry.js +15 -14
  69. package/dist/dxt-entry.js.map +1 -1
  70. package/dist/index.d.ts +3 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +37 -21
  73. package/dist/index.js.map +1 -1
  74. package/dist/performance/MetricsCollector.d.ts +13 -7
  75. package/dist/performance/MetricsCollector.d.ts.map +1 -1
  76. package/dist/performance/MetricsCollector.js +69 -27
  77. package/dist/performance/MetricsCollector.js.map +1 -1
  78. package/dist/performance/PerformanceAnalytics.d.ts +8 -2
  79. package/dist/performance/PerformanceAnalytics.d.ts.map +1 -1
  80. package/dist/performance/PerformanceAnalytics.js +17 -47
  81. package/dist/performance/PerformanceAnalytics.js.map +1 -1
  82. package/dist/performance/PerformanceMonitor.d.ts +2 -1
  83. package/dist/performance/PerformanceMonitor.d.ts.map +1 -1
  84. package/dist/performance/PerformanceMonitor.js +12 -13
  85. package/dist/performance/PerformanceMonitor.js.map +1 -1
  86. package/dist/performance/index.d.ts +2 -2
  87. package/dist/performance/index.d.ts.map +1 -1
  88. package/dist/security/AISecurityScanner.d.ts +1 -0
  89. package/dist/security/AISecurityScanner.d.ts.map +1 -1
  90. package/dist/security/AISecurityScanner.js +22 -12
  91. package/dist/security/AISecurityScanner.js.map +1 -1
  92. package/dist/security/AutomatedRemediation.d.ts +4 -3
  93. package/dist/security/AutomatedRemediation.d.ts.map +1 -1
  94. package/dist/security/AutomatedRemediation.js +46 -15
  95. package/dist/security/AutomatedRemediation.js.map +1 -1
  96. package/dist/security/InputValidator.d.ts +13 -9
  97. package/dist/security/InputValidator.d.ts.map +1 -1
  98. package/dist/security/InputValidator.js +4 -2
  99. package/dist/security/InputValidator.js.map +1 -1
  100. package/dist/security/SecurityCIPipeline.d.ts +1 -1
  101. package/dist/security/SecurityCIPipeline.d.ts.map +1 -1
  102. package/dist/security/SecurityCIPipeline.js +38 -29
  103. package/dist/security/SecurityCIPipeline.js.map +1 -1
  104. package/dist/security/SecurityConfig.d.ts +3 -3
  105. package/dist/security/SecurityConfig.d.ts.map +1 -1
  106. package/dist/security/SecurityConfig.js +13 -9
  107. package/dist/security/SecurityConfig.js.map +1 -1
  108. package/dist/security/SecurityConfigManager.d.ts +2 -2
  109. package/dist/security/SecurityConfigManager.d.ts.map +1 -1
  110. package/dist/security/SecurityConfigManager.js +20 -15
  111. package/dist/security/SecurityConfigManager.js.map +1 -1
  112. package/dist/security/SecurityMonitoring.d.ts +2 -2
  113. package/dist/security/SecurityMonitoring.d.ts.map +1 -1
  114. package/dist/security/SecurityMonitoring.js +19 -17
  115. package/dist/security/SecurityMonitoring.js.map +1 -1
  116. package/dist/security/SecurityReviewer.d.ts.map +1 -1
  117. package/dist/security/SecurityReviewer.js +10 -7
  118. package/dist/security/SecurityReviewer.js.map +1 -1
  119. package/dist/security/index.d.ts +24 -23
  120. package/dist/security/index.d.ts.map +1 -1
  121. package/dist/security/index.js +52 -23
  122. package/dist/security/index.js.map +1 -1
  123. package/dist/server/ConnectionTester.d.ts +12 -4
  124. package/dist/server/ConnectionTester.d.ts.map +1 -1
  125. package/dist/server/ConnectionTester.js +96 -22
  126. package/dist/server/ConnectionTester.js.map +1 -1
  127. package/dist/server/ToolRegistry.d.ts +2 -2
  128. package/dist/server/ToolRegistry.d.ts.map +1 -1
  129. package/dist/server/ToolRegistry.js +10 -5
  130. package/dist/server/ToolRegistry.js.map +1 -1
  131. package/dist/tools/BaseToolManager.d.ts +47 -11
  132. package/dist/tools/BaseToolManager.d.ts.map +1 -1
  133. package/dist/tools/BaseToolManager.js +168 -29
  134. package/dist/tools/BaseToolManager.js.map +1 -1
  135. package/dist/tools/auth.d.ts +16 -10
  136. package/dist/tools/auth.d.ts.map +1 -1
  137. package/dist/tools/auth.js +3 -2
  138. package/dist/tools/auth.js.map +1 -1
  139. package/dist/tools/cache.d.ts +30 -30
  140. package/dist/tools/cache.d.ts.map +1 -1
  141. package/dist/tools/cache.js +1 -6
  142. package/dist/tools/cache.js.map +1 -1
  143. package/dist/tools/comments.d.ts +20 -20
  144. package/dist/tools/comments.d.ts.map +1 -1
  145. package/dist/tools/comments.js +16 -9
  146. package/dist/tools/comments.js.map +1 -1
  147. package/dist/tools/media.d.ts +18 -16
  148. package/dist/tools/media.d.ts.map +1 -1
  149. package/dist/tools/media.js +16 -15
  150. package/dist/tools/media.js.map +1 -1
  151. package/dist/tools/pages.d.ts +19 -17
  152. package/dist/tools/pages.d.ts.map +1 -1
  153. package/dist/tools/pages.js +16 -12
  154. package/dist/tools/pages.js.map +1 -1
  155. package/dist/tools/performance.d.ts +11 -1
  156. package/dist/tools/performance.d.ts.map +1 -1
  157. package/dist/tools/performance.js +67 -34
  158. package/dist/tools/performance.js.map +1 -1
  159. package/dist/tools/posts/PostHandlers.d.ts +46 -0
  160. package/dist/tools/posts/PostHandlers.d.ts.map +1 -0
  161. package/dist/tools/posts/PostHandlers.js +400 -0
  162. package/dist/tools/posts/PostHandlers.js.map +1 -0
  163. package/dist/tools/posts/PostToolDefinitions.d.ts +37 -0
  164. package/dist/tools/posts/PostToolDefinitions.d.ts.map +1 -0
  165. package/dist/tools/posts/PostToolDefinitions.js +236 -0
  166. package/dist/tools/posts/PostToolDefinitions.js.map +1 -0
  167. package/dist/tools/posts/index.d.ts +138 -0
  168. package/dist/tools/posts/index.d.ts.map +1 -0
  169. package/dist/tools/posts/index.js +163 -0
  170. package/dist/tools/posts/index.js.map +1 -0
  171. package/dist/tools/posts.d.ts +10 -246
  172. package/dist/tools/posts.d.ts.map +1 -1
  173. package/dist/tools/posts.js +11 -723
  174. package/dist/tools/posts.js.map +1 -1
  175. package/dist/tools/site.d.ts +19 -18
  176. package/dist/tools/site.d.ts.map +1 -1
  177. package/dist/tools/site.js +14 -10
  178. package/dist/tools/site.js.map +1 -1
  179. package/dist/tools/taxonomies.d.ts +23 -24
  180. package/dist/tools/taxonomies.d.ts.map +1 -1
  181. package/dist/tools/taxonomies.js +24 -18
  182. package/dist/tools/taxonomies.js.map +1 -1
  183. package/dist/tools/users.d.ts +20 -15
  184. package/dist/tools/users.d.ts.map +1 -1
  185. package/dist/tools/users.js +12 -8
  186. package/dist/tools/users.js.map +1 -1
  187. package/dist/types/client.d.ts +48 -41
  188. package/dist/types/client.d.ts.map +1 -1
  189. package/dist/types/client.js +30 -5
  190. package/dist/types/client.js.map +1 -1
  191. package/dist/types/enhanced.d.ts +237 -0
  192. package/dist/types/enhanced.d.ts.map +1 -0
  193. package/dist/types/enhanced.js +49 -0
  194. package/dist/types/enhanced.js.map +1 -0
  195. package/dist/types/index.d.ts +15 -12
  196. package/dist/types/index.d.ts.map +1 -1
  197. package/dist/types/index.js +2 -0
  198. package/dist/types/index.js.map +1 -1
  199. package/dist/types/mcp.d.ts +12 -12
  200. package/dist/types/mcp.d.ts.map +1 -1
  201. package/dist/types/requests.d.ts +322 -0
  202. package/dist/types/requests.d.ts.map +1 -0
  203. package/dist/types/requests.js +8 -0
  204. package/dist/types/requests.js.map +1 -0
  205. package/dist/types/tools.d.ts +506 -0
  206. package/dist/types/tools.d.ts.map +1 -0
  207. package/dist/types/tools.js +8 -0
  208. package/dist/types/tools.js.map +1 -0
  209. package/dist/types/wordpress.d.ts +43 -15
  210. package/dist/types/wordpress.d.ts.map +1 -1
  211. package/dist/types/wordpress.js +8 -1
  212. package/dist/types/wordpress.js.map +1 -1
  213. package/dist/utils/debug.d.ts +19 -11
  214. package/dist/utils/debug.d.ts.map +1 -1
  215. package/dist/utils/debug.js +46 -10
  216. package/dist/utils/debug.js.map +1 -1
  217. package/dist/utils/enhancedError.d.ts +8 -8
  218. package/dist/utils/enhancedError.d.ts.map +1 -1
  219. package/dist/utils/enhancedError.js.map +1 -1
  220. package/dist/utils/error.d.ts +2 -4
  221. package/dist/utils/error.d.ts.map +1 -1
  222. package/dist/utils/error.js +42 -5
  223. package/dist/utils/error.js.map +1 -1
  224. package/dist/utils/logger.d.ts +106 -0
  225. package/dist/utils/logger.d.ts.map +1 -0
  226. package/dist/utils/logger.js +280 -0
  227. package/dist/utils/logger.js.map +1 -0
  228. package/dist/utils/streaming.d.ts +9 -9
  229. package/dist/utils/streaming.d.ts.map +1 -1
  230. package/dist/utils/streaming.js +71 -52
  231. package/dist/utils/streaming.js.map +1 -1
  232. package/dist/utils/toolWrapper.d.ts +9 -7
  233. package/dist/utils/toolWrapper.d.ts.map +1 -1
  234. package/dist/utils/toolWrapper.js.map +1 -1
  235. package/dist/utils/validation/core.d.ts +21 -0
  236. package/dist/utils/validation/core.d.ts.map +1 -0
  237. package/dist/utils/validation/core.js +71 -0
  238. package/dist/utils/validation/core.js.map +1 -0
  239. package/dist/utils/validation/index.d.ts +25 -0
  240. package/dist/utils/validation/index.d.ts.map +1 -0
  241. package/dist/utils/validation/index.js +29 -0
  242. package/dist/utils/validation/index.js.map +1 -0
  243. package/dist/utils/validation/network.d.ts +19 -0
  244. package/dist/utils/validation/network.d.ts.map +1 -0
  245. package/dist/utils/validation/network.js +93 -0
  246. package/dist/utils/validation/network.js.map +1 -0
  247. package/dist/utils/validation/rateLimit.d.ts +21 -0
  248. package/dist/utils/validation/rateLimit.d.ts.map +1 -0
  249. package/dist/utils/validation/rateLimit.js +43 -0
  250. package/dist/utils/validation/rateLimit.js.map +1 -0
  251. package/dist/utils/validation/security.d.ts +29 -0
  252. package/dist/utils/validation/security.d.ts.map +1 -0
  253. package/dist/utils/validation/security.js +327 -0
  254. package/dist/utils/validation/security.js.map +1 -0
  255. package/dist/utils/validation/wordpress.d.ts +31 -0
  256. package/dist/utils/validation/wordpress.d.ts.map +1 -0
  257. package/dist/utils/validation/wordpress.js +146 -0
  258. package/dist/utils/validation/wordpress.js.map +1 -0
  259. package/dist/utils/validation.d.ts +13 -82
  260. package/dist/utils/validation.d.ts.map +1 -1
  261. package/dist/utils/validation.js +25 -343
  262. package/dist/utils/validation.js.map +1 -1
  263. package/docs/BADGE_UPDATES.md +132 -0
  264. package/docs/CI_CD_IMPROVEMENTS.md +191 -0
  265. package/docs/INCREMENTAL_COVERAGE.md +183 -0
  266. package/docs/api/README.md +3 -1
  267. package/docs/api/openapi.json +5 -1
  268. package/docs/api/summary.json +1 -1
  269. package/docs/api/tools/wp_create_post.md +12 -14
  270. package/docs/examples/claude-desktop-config.md +1 -1
  271. package/docs/examples/docker-production.md +100 -93
  272. package/docs/examples/multi-site-setup.md +5 -4
  273. package/docs/examples/single-site-setup.md +3 -4
  274. package/docs/examples/use-case-workflows.md +4 -5
  275. package/docs/integrations/claude-desktop.md +31 -31
  276. package/docs/integrations/cline.md +4 -4
  277. package/docs/integrations/vs-code.md +9 -8
  278. package/docs/user-guides/SMITHERY_SETUP.md +10 -10
  279. package/package.json +44 -25
  280. package/src/cache/CacheInvalidation.ts +12 -5
  281. package/src/cache/CacheManager.ts +18 -15
  282. package/src/cache/HttpCacheWrapper.ts +30 -59
  283. package/src/cache/__tests__/HttpCacheWrapper.test.ts +6 -5
  284. package/src/cache/index.ts +3 -14
  285. package/src/client/CachedWordPressClient.ts +32 -30
  286. package/src/client/MockWordPressClient.ts +4 -2
  287. package/src/client/api.ts +186 -64
  288. package/src/client/auth.ts +15 -40
  289. package/src/client/managers/AuthenticationManager.ts +337 -77
  290. package/src/client/managers/BaseManager.ts +18 -30
  291. package/src/client/managers/RequestManager.ts +39 -44
  292. package/src/config/Config.ts +308 -0
  293. package/src/config/ConfigurationSchema.ts +23 -2
  294. package/src/config/ServerConfiguration.ts +51 -47
  295. package/src/docs/DocumentationGenerator.ts +50 -39
  296. package/src/docs/MarkdownFormatter.ts +19 -29
  297. package/src/dxt-entry.cjs +26 -16
  298. package/src/dxt-entry.ts +17 -27
  299. package/src/index.ts +42 -28
  300. package/src/performance/MetricsCollector.ts +108 -86
  301. package/src/performance/PerformanceAnalytics.ts +69 -164
  302. package/src/performance/PerformanceMonitor.ts +32 -47
  303. package/src/performance/index.ts +2 -10
  304. package/src/security/AISecurityScanner.ts +22 -12
  305. package/src/security/AutomatedRemediation.ts +49 -18
  306. package/src/security/InputValidator.ts +9 -6
  307. package/src/security/SecurityCIPipeline.ts +53 -37
  308. package/src/security/SecurityConfig.ts +22 -22
  309. package/src/security/SecurityConfigManager.ts +23 -19
  310. package/src/security/SecurityMonitoring.ts +24 -21
  311. package/src/security/SecurityReviewer.ts +10 -7
  312. package/src/security/index.ts +64 -29
  313. package/src/server/ConnectionTester.ts +120 -31
  314. package/src/server/ToolRegistry.ts +31 -21
  315. package/src/tools/BaseToolManager.ts +286 -33
  316. package/src/tools/auth.ts +20 -8
  317. package/src/tools/cache.ts +5 -15
  318. package/src/tools/comments.ts +34 -48
  319. package/src/tools/media.ts +41 -53
  320. package/src/tools/pages.ts +32 -54
  321. package/src/tools/performance.ts +141 -176
  322. package/src/tools/posts/PostHandlers.ts +474 -0
  323. package/src/tools/posts/PostToolDefinitions.ts +250 -0
  324. package/src/tools/posts/index.ts +192 -0
  325. package/src/tools/posts.ts +24 -780
  326. package/src/tools/site.ts +34 -19
  327. package/src/tools/taxonomies.ts +41 -57
  328. package/src/tools/users.ts +28 -16
  329. package/src/types/client.ts +114 -138
  330. package/src/types/enhanced.ts +318 -0
  331. package/src/types/index.ts +51 -30
  332. package/src/types/mcp.ts +20 -42
  333. package/src/types/requests.ts +378 -0
  334. package/src/types/tools.ts +608 -0
  335. package/src/types/wordpress.ts +56 -34
  336. package/src/utils/debug.ts +77 -59
  337. package/src/utils/enhancedError.ts +8 -8
  338. package/src/utils/error.ts +53 -31
  339. package/src/utils/logger.ts +351 -0
  340. package/src/utils/streaming.ts +86 -68
  341. package/src/utils/toolWrapper.ts +10 -12
  342. package/src/utils/validation/core.ts +108 -0
  343. package/src/utils/validation/index.ts +36 -0
  344. package/src/utils/validation/network.ts +132 -0
  345. package/src/utils/validation/rateLimit.ts +54 -0
  346. package/src/utils/validation/security.ts +361 -0
  347. package/src/utils/validation/wordpress.ts +180 -0
  348. package/src/utils/validation.ts +47 -470
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Security Validation Utilities
3
+ *
4
+ * Validation functions focused on security concerns including file path validation,
5
+ * file size limits, MIME type validation, and HTML sanitization.
6
+ */
7
+
8
+ import * as path from "path";
9
+ import { WordPressAPIError } from "../../types/client.js";
10
+
11
+ /**
12
+ * Validates and sanitizes file paths to prevent directory traversal
13
+ */
14
+ export function validateFilePath(userPath: string, allowedBasePath: string): string {
15
+ // Normalize the path to remove ../ and other dangerous patterns
16
+ const normalizedPath = path.normalize(userPath);
17
+ const resolvedPath = path.resolve(allowedBasePath, normalizedPath);
18
+
19
+ // Ensure the resolved path is within the allowed directory
20
+ if (!resolvedPath.startsWith(path.resolve(allowedBasePath))) {
21
+ throw new WordPressAPIError("Invalid file path: access denied", 403, "PATH_TRAVERSAL_ATTEMPT");
22
+ }
23
+
24
+ return resolvedPath;
25
+ }
26
+
27
+ /**
28
+ * Validates file size
29
+ */
30
+ export function validateFileSize(sizeInBytes: number, maxSizeInMB: number = 10): void {
31
+ const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
32
+ if (sizeInBytes > maxSizeInBytes) {
33
+ throw new WordPressAPIError(`File size exceeds maximum allowed size of ${maxSizeInMB}MB`, 413, "FILE_TOO_LARGE");
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Validates MIME types for file uploads
39
+ */
40
+ export function validateMimeType(mimeType: string, allowedTypes: string[]): void {
41
+ if (!allowedTypes.includes(mimeType)) {
42
+ throw new WordPressAPIError(
43
+ `Invalid file type: ${mimeType}. Allowed types: ${allowedTypes.join(", ")}`,
44
+ 415,
45
+ "UNSUPPORTED_MEDIA_TYPE",
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Sanitizes HTML content to prevent XSS attacks using a whitelist-based approach
52
+ *
53
+ * This implementation satisfies GitHub Advanced Security requirements by:
54
+ * 1. Using character-by-character parsing instead of potentially vulnerable regex patterns
55
+ * 2. Implementing a strict whitelist of allowed elements and attributes
56
+ * 3. Properly handling all edge cases that bypass traditional regex sanitization
57
+ * 4. Providing complete protection against script injection, event handlers, and dangerous protocols
58
+ */
59
+ export function sanitizeHtml(html: string): string {
60
+ if (!html || typeof html !== "string") {
61
+ return "";
62
+ }
63
+
64
+ // Whitelist of allowed HTML elements (safe for WordPress content)
65
+ const allowedElements = new Set([
66
+ "p",
67
+ "br",
68
+ "strong",
69
+ "b",
70
+ "em",
71
+ "i",
72
+ "u",
73
+ "span",
74
+ "div",
75
+ "h1",
76
+ "h2",
77
+ "h3",
78
+ "h4",
79
+ "h5",
80
+ "h6",
81
+ "ul",
82
+ "ol",
83
+ "li",
84
+ "dl",
85
+ "dt",
86
+ "dd",
87
+ "blockquote",
88
+ "pre",
89
+ "code",
90
+ "a",
91
+ "img",
92
+ "table",
93
+ "thead",
94
+ "tbody",
95
+ "tfoot",
96
+ "tr",
97
+ "td",
98
+ "th",
99
+ "hr",
100
+ ]);
101
+
102
+ // Whitelist of allowed attributes per element
103
+ const allowedAttributes: Record<string, Set<string>> = {
104
+ a: new Set(["href", "title", "target"]),
105
+ img: new Set(["src", "alt", "title", "width", "height"]),
106
+ div: new Set(["class", "id"]),
107
+ span: new Set(["class", "id"]),
108
+ p: new Set(["class", "id"]),
109
+ h1: new Set(["class", "id"]),
110
+ h2: new Set(["class", "id"]),
111
+ h3: new Set(["class", "id"]),
112
+ h4: new Set(["class", "id"]),
113
+ h5: new Set(["class", "id"]),
114
+ h6: new Set(["class", "id"]),
115
+ table: new Set(["class", "id"]),
116
+ td: new Set(["class", "id", "colspan", "rowspan"]),
117
+ th: new Set(["class", "id", "colspan", "rowspan"]),
118
+ };
119
+
120
+ // Safe URL protocols
121
+ const safeProtocols = new Set(["http:", "https:", "mailto:", "tel:", "ftp:"]);
122
+
123
+ let result = "";
124
+ let i = 0;
125
+
126
+ while (i < html.length) {
127
+ if (html[i] === "<") {
128
+ // Found a potential HTML tag
129
+ const tagMatch = parseHtmlTag(html, i);
130
+
131
+ if (tagMatch) {
132
+ const { tagName, attributes, isClosing, isSelfClosing, endIndex } = tagMatch;
133
+
134
+ // Check if this is an allowed element
135
+ if (allowedElements.has(tagName.toLowerCase())) {
136
+ // Build sanitized tag
137
+ let sanitizedTag = "<";
138
+ if (isClosing) sanitizedTag += "/";
139
+ sanitizedTag += tagName.toLowerCase();
140
+
141
+ // Process attributes if not a closing tag
142
+ if (!isClosing && attributes.length > 0) {
143
+ const allowedAttrs = allowedAttributes[tagName.toLowerCase()] || new Set();
144
+
145
+ for (const attr of attributes) {
146
+ if (allowedAttrs.has(attr.name.toLowerCase())) {
147
+ // Additional validation for URL attributes
148
+ if ((attr.name.toLowerCase() === "href" || attr.name.toLowerCase() === "src") && attr.value) {
149
+ if (isValidUrl(attr.value, safeProtocols)) {
150
+ sanitizedTag += ` ${attr.name.toLowerCase()}="${escapeAttributeValue(attr.value)}"`;
151
+ }
152
+ } else if (attr.value !== null) {
153
+ // Other safe attributes
154
+ sanitizedTag += ` ${attr.name.toLowerCase()}="${escapeAttributeValue(attr.value)}"`;
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ if (isSelfClosing) sanitizedTag += " /";
161
+ sanitizedTag += ">";
162
+
163
+ result += sanitizedTag;
164
+ }
165
+ // If element not allowed, skip the entire tag
166
+
167
+ i = endIndex;
168
+ } else {
169
+ // Not a valid tag, treat as text
170
+ result += escapeHtmlChar(html[i]);
171
+ i++;
172
+ }
173
+ } else {
174
+ // Regular text content
175
+ result += escapeHtmlChar(html[i]);
176
+ i++;
177
+ }
178
+ }
179
+
180
+ // Final cleanup: normalize whitespace
181
+ return result.replace(/\s+/g, " ").trim();
182
+ }
183
+
184
+ /**
185
+ * Parses an HTML tag starting at the given position
186
+ * Returns tag information or null if not a valid tag
187
+ */
188
+ function parseHtmlTag(
189
+ html: string,
190
+ startIndex: number,
191
+ ): {
192
+ tagName: string;
193
+ attributes: Array<{ name: string; value: string | null }>;
194
+ isClosing: boolean;
195
+ isSelfClosing: boolean;
196
+ endIndex: number;
197
+ } | null {
198
+ if (html[startIndex] !== "<") return null;
199
+
200
+ let i = startIndex + 1;
201
+ let isClosing = false;
202
+
203
+ // Skip whitespace
204
+ while (i < html.length && /\s/.test(html[i])) i++;
205
+
206
+ // Check for closing tag
207
+ if (i < html.length && html[i] === "/") {
208
+ isClosing = true;
209
+ i++;
210
+ while (i < html.length && /\s/.test(html[i])) i++;
211
+ }
212
+
213
+ // Parse tag name
214
+ const tagNameStart = i;
215
+ while (i < html.length && /[a-zA-Z0-9]/.test(html[i])) i++;
216
+
217
+ if (i === tagNameStart) return null; // No valid tag name
218
+
219
+ const tagName = html.substring(tagNameStart, i);
220
+ const attributes: Array<{ name: string; value: string | null }> = [];
221
+
222
+ // Parse attributes (only for opening tags)
223
+ if (!isClosing) {
224
+ while (i < html.length && html[i] !== ">") {
225
+ // Skip whitespace
226
+ while (i < html.length && /\s/.test(html[i])) i++;
227
+
228
+ if (i >= html.length || html[i] === ">") break;
229
+
230
+ // Check for self-closing
231
+ if (html[i] === "/") {
232
+ i++;
233
+ while (i < html.length && /\s/.test(html[i])) i++;
234
+ if (i < html.length && html[i] === ">") {
235
+ return {
236
+ tagName,
237
+ attributes,
238
+ isClosing: false,
239
+ isSelfClosing: true,
240
+ endIndex: i + 1,
241
+ };
242
+ }
243
+ break;
244
+ }
245
+
246
+ // Parse attribute name
247
+ const attrNameStart = i;
248
+ while (i < html.length && /[a-zA-Z0-9-_]/.test(html[i])) i++;
249
+
250
+ if (i === attrNameStart) break; // Invalid attribute name
251
+
252
+ const attrName = html.substring(attrNameStart, i);
253
+ let attrValue: string | null = null;
254
+
255
+ // Skip whitespace
256
+ while (i < html.length && /\s/.test(html[i])) i++;
257
+
258
+ // Check for attribute value
259
+ if (i < html.length && html[i] === "=") {
260
+ i++;
261
+ while (i < html.length && /\s/.test(html[i])) i++;
262
+
263
+ if (i < html.length) {
264
+ if (html[i] === '"' || html[i] === "'") {
265
+ // Quoted value
266
+ const quote = html[i];
267
+ i++;
268
+ const valueStart = i;
269
+ while (i < html.length && html[i] !== quote) i++;
270
+ if (i < html.length) {
271
+ attrValue = html.substring(valueStart, i);
272
+ i++; // Skip closing quote
273
+ }
274
+ } else {
275
+ // Unquoted value
276
+ const valueStart = i;
277
+ while (i < html.length && !/[\s>]/.test(html[i])) i++;
278
+ attrValue = html.substring(valueStart, i);
279
+ }
280
+ }
281
+ }
282
+
283
+ attributes.push({ name: attrName, value: attrValue });
284
+ }
285
+ }
286
+
287
+ // Find closing >
288
+ while (i < html.length && html[i] !== ">") i++;
289
+ if (i >= html.length) return null; // Unclosed tag
290
+
291
+ return {
292
+ tagName,
293
+ attributes,
294
+ isClosing,
295
+ isSelfClosing: false,
296
+ endIndex: i + 1,
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Validates URL safety
302
+ */
303
+ function isValidUrl(url: string, safeProtocols: Set<string>): boolean {
304
+ if (!url || typeof url !== "string") return false;
305
+
306
+ // Remove whitespace
307
+ url = url.trim();
308
+ if (!url) return false;
309
+
310
+ // Allow relative URLs
311
+ if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
312
+ return true;
313
+ }
314
+
315
+ // Allow fragment URLs
316
+ if (url.startsWith("#")) {
317
+ return true;
318
+ }
319
+
320
+ // Check protocol
321
+ const protocolMatch = url.match(/^([a-z][a-z0-9+.-]*:)/i);
322
+ if (protocolMatch) {
323
+ return safeProtocols.has(protocolMatch[1].toLowerCase());
324
+ }
325
+
326
+ // No protocol specified, treat as relative
327
+ return true;
328
+ }
329
+
330
+ /**
331
+ * Escapes attribute values
332
+ */
333
+ function escapeAttributeValue(value: string): string {
334
+ if (!value) return "";
335
+ return value
336
+ .replace(/&/g, "&amp;")
337
+ .replace(/"/g, "&quot;")
338
+ .replace(/'/g, "&#39;")
339
+ .replace(/</g, "&lt;")
340
+ .replace(/>/g, "&gt;");
341
+ }
342
+
343
+ /**
344
+ * Escapes individual HTML characters
345
+ */
346
+ function escapeHtmlChar(char: string): string {
347
+ switch (char) {
348
+ case "&":
349
+ return "&amp;";
350
+ case "<":
351
+ return "&lt;";
352
+ case ">":
353
+ return "&gt;";
354
+ case '"':
355
+ return "&quot;";
356
+ case "'":
357
+ return "&#39;";
358
+ default:
359
+ return char;
360
+ }
361
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * WordPress-Specific Validation Utilities
3
+ *
4
+ * Validation functions specific to WordPress data structures including post parameters,
5
+ * pagination, search queries, and status validation.
6
+ */
7
+
8
+ import { WordPressAPIError } from "../../types/client.js";
9
+ import { validateId } from "./core.js";
10
+ import { validateString, validateArray } from "./core.js";
11
+ import { sanitizeHtml } from "./security.js";
12
+
13
+ /**
14
+ * Validates WordPress post status values
15
+ */
16
+ export function validatePostStatus(status: string): string {
17
+ const validStatuses = ["publish", "draft", "pending", "private", "future", "auto-draft", "trash"];
18
+ if (!validStatuses.includes(status)) {
19
+ throw new WordPressAPIError(`Invalid status: must be one of ${validStatuses.join(", ")}`, 400, "INVALID_PARAMETER");
20
+ }
21
+ return status;
22
+ }
23
+
24
+ /**
25
+ * Validates and sanitizes search queries
26
+ */
27
+ export function validateSearchQuery(query: string): string {
28
+ // Remove potentially dangerous characters while preserving search functionality
29
+ let sanitized = query.trim();
30
+
31
+ // Limit length to prevent DoS
32
+ if (sanitized.length > 200) {
33
+ sanitized = sanitized.substring(0, 200);
34
+ }
35
+
36
+ // Remove SQL-like patterns (basic protection)
37
+ sanitized = sanitized.replace(/(\b(union|select|insert|update|delete|drop|create)\b)/gi, "");
38
+
39
+ // Remove special characters that might be used for injection
40
+ sanitized = sanitized.replace(/[<>'"`;\\]/g, "");
41
+
42
+ return sanitized;
43
+ }
44
+
45
+ /**
46
+ * Validates pagination parameters as a set
47
+ */
48
+ export function validatePaginationParams(params: { page?: unknown; per_page?: unknown; offset?: unknown }): {
49
+ page?: number;
50
+ per_page?: number;
51
+ offset?: number;
52
+ } {
53
+ const validated: { page?: number; per_page?: number; offset?: number } = {};
54
+
55
+ // Validate page
56
+ if (params.page !== undefined) {
57
+ const page = parseInt(String(params.page), 10);
58
+ if (isNaN(page) || page < 1) {
59
+ throw new WordPressAPIError("Page must be a positive integer", 400, "INVALID_PARAMETER");
60
+ }
61
+ if (page > 10000) {
62
+ throw new WordPressAPIError("Page number too high (max 10000)", 400, "INVALID_PARAMETER");
63
+ }
64
+ validated.page = page;
65
+ }
66
+
67
+ // Validate per_page
68
+ if (params.per_page !== undefined) {
69
+ const perPage = parseInt(String(params.per_page), 10);
70
+ if (isNaN(perPage) || perPage < 1) {
71
+ throw new WordPressAPIError("Per page must be a positive integer", 400, "INVALID_PARAMETER");
72
+ }
73
+ if (perPage > 100) {
74
+ throw new WordPressAPIError(`Per page exceeds maximum allowed (100), got ${perPage}`, 400, "INVALID_PARAMETER");
75
+ }
76
+ validated.per_page = perPage;
77
+ }
78
+
79
+ // Validate offset
80
+ if (params.offset !== undefined) {
81
+ const offset = parseInt(String(params.offset), 10);
82
+ if (isNaN(offset) || offset < 0) {
83
+ throw new WordPressAPIError("Offset must be a non-negative integer", 400, "INVALID_PARAMETER");
84
+ }
85
+ if (offset > 1000000) {
86
+ throw new WordPressAPIError("Offset too large (max 1000000)", 400, "INVALID_PARAMETER");
87
+ }
88
+ validated.offset = offset;
89
+ }
90
+
91
+ // Check for conflicting parameters
92
+ if (validated.page && validated.offset) {
93
+ throw new WordPressAPIError(
94
+ "Cannot use both 'page' and 'offset' parameters together",
95
+ 400,
96
+ "CONFLICTING_PARAMETERS",
97
+ );
98
+ }
99
+
100
+ return validated;
101
+ }
102
+
103
+ /**
104
+ * Validates complex post creation parameters
105
+ */
106
+ export function validatePostParams(params: unknown): Record<string, unknown> {
107
+ const validated: Record<string, unknown> = {};
108
+
109
+ // Type guard to ensure params is an object
110
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
111
+ throw new WordPressAPIError("Post parameters must be an object", 400, "INVALID_PARAMETER");
112
+ }
113
+
114
+ const typedParams = params as Record<string, unknown>;
115
+
116
+ // Title validation
117
+ if (!typedParams.title || typeof typedParams.title !== "string") {
118
+ throw new WordPressAPIError("Post title is required and must be a string", 400, "INVALID_PARAMETER");
119
+ }
120
+ validated.title = validateString(typedParams.title, "title", 1, 200);
121
+
122
+ // Content validation
123
+ if (typedParams.content !== undefined) {
124
+ validated.content = sanitizeHtml(String(typedParams.content));
125
+ }
126
+
127
+ // Status validation with context
128
+ if (typedParams.status) {
129
+ if (typeof typedParams.status !== "string") {
130
+ throw new WordPressAPIError("Status must be a string", 400, "INVALID_PARAMETER");
131
+ }
132
+ validated.status = validatePostStatus(typedParams.status);
133
+
134
+ // Future posts need a date
135
+ if (validated.status === "future" && !typedParams.date) {
136
+ throw new WordPressAPIError("Future posts require a 'date' parameter", 400, "MISSING_PARAMETER");
137
+ }
138
+ }
139
+
140
+ // Categories and tags validation
141
+ if (typedParams.categories) {
142
+ const categories = validateArray<unknown>(typedParams.categories, "categories", 0, 50);
143
+ validated.categories = categories.map((id: unknown) => validateId(id, "category ID"));
144
+ }
145
+
146
+ if (typedParams.tags) {
147
+ const tags = validateArray<unknown>(typedParams.tags, "tags", 0, 100);
148
+ validated.tags = tags.map((id: unknown) => validateId(id, "tag ID"));
149
+ }
150
+
151
+ // Featured media validation
152
+ if (typedParams.featured_media !== undefined) {
153
+ if (typedParams.featured_media === null || typedParams.featured_media === 0) {
154
+ // Allow null or 0 to remove featured media
155
+ validated.featured_media = 0;
156
+ } else {
157
+ validated.featured_media = validateId(typedParams.featured_media, "featured_media");
158
+ }
159
+ }
160
+
161
+ // Date validation for scheduled posts
162
+ if (typedParams.date) {
163
+ try {
164
+ const date = new Date(String(typedParams.date));
165
+ if (isNaN(date.getTime())) {
166
+ throw new Error("Invalid date");
167
+ }
168
+ // WordPress expects ISO 8601 format
169
+ validated.date = date.toISOString();
170
+ } catch {
171
+ throw new WordPressAPIError(
172
+ "Invalid date format. Use ISO 8601 format (YYYY-MM-DDTHH:mm:ss)",
173
+ 400,
174
+ "INVALID_PARAMETER",
175
+ );
176
+ }
177
+ }
178
+
179
+ return validated;
180
+ }