mcp-wordpress 1.1.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 +21 -0
- package/README.md +568 -0
- package/bin/mcp-wordpress.js +12 -0
- package/bin/setup.js +302 -0
- package/bin/status.js +359 -0
- package/dist/client/WordPressClient.d.ts +81 -0
- package/dist/client/WordPressClient.d.ts.map +1 -0
- package/dist/client/WordPressClient.js +354 -0
- package/dist/client/WordPressClient.js.map +1 -0
- package/dist/client/api.d.ts +140 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +727 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/auth.d.ts +121 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +430 -0
- package/dist/client/auth.js.map +1 -0
- package/dist/client/managers/AuthenticationManager.d.ts +39 -0
- package/dist/client/managers/AuthenticationManager.d.ts.map +1 -0
- package/dist/client/managers/AuthenticationManager.js +159 -0
- package/dist/client/managers/AuthenticationManager.js.map +1 -0
- package/dist/client/managers/BaseManager.d.ts +22 -0
- package/dist/client/managers/BaseManager.d.ts.map +1 -0
- package/dist/client/managers/BaseManager.js +47 -0
- package/dist/client/managers/BaseManager.js.map +1 -0
- package/dist/client/managers/RequestManager.d.ts +45 -0
- package/dist/client/managers/RequestManager.d.ts.map +1 -0
- package/dist/client/managers/RequestManager.js +161 -0
- package/dist/client/managers/RequestManager.js.map +1 -0
- package/dist/client/managers/index.d.ts +8 -0
- package/dist/client/managers/index.d.ts.map +1 -0
- package/dist/client/managers/index.js +8 -0
- package/dist/client/managers/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +264 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/auth.d.ts +44 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +126 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/tools/base.d.ts +37 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +60 -0
- package/dist/tools/base.js.map +1 -0
- package/dist/tools/comments.d.ts +33 -0
- package/dist/tools/comments.d.ts.map +1 -0
- package/dist/tools/comments.js +228 -0
- package/dist/tools/comments.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/media.d.ts +29 -0
- package/dist/tools/media.d.ts.map +1 -0
- package/dist/tools/media.js +208 -0
- package/dist/tools/media.js.map +1 -0
- package/dist/tools/pages.d.ts +30 -0
- package/dist/tools/pages.d.ts.map +1 -0
- package/dist/tools/pages.js +211 -0
- package/dist/tools/pages.js.map +1 -0
- package/dist/tools/posts.d.ts +30 -0
- package/dist/tools/posts.d.ts.map +1 -0
- package/dist/tools/posts.js +240 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/site.d.ts +31 -0
- package/dist/tools/site.d.ts.map +1 -0
- package/dist/tools/site.js +192 -0
- package/dist/tools/site.js.map +1 -0
- package/dist/tools/taxonomies.d.ts +37 -0
- package/dist/tools/taxonomies.d.ts.map +1 -0
- package/dist/tools/taxonomies.js +280 -0
- package/dist/tools/taxonomies.js.map +1 -0
- package/dist/tools/users.d.ts +28 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +201 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/types/client.d.ts +215 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/client.js +72 -0
- package/dist/types/client.js.map +1 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +178 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +7 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/wordpress.d.ts +443 -0
- package/dist/types/wordpress.d.ts.map +1 -0
- package/dist/types/wordpress.js +7 -0
- package/dist/types/wordpress.js.map +1 -0
- package/dist/utils/debug.d.ts +63 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +195 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/error.d.ts +19 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +71 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/toolWrapper.d.ts +36 -0
- package/dist/utils/toolWrapper.d.ts.map +1 -0
- package/dist/utils/toolWrapper.js +90 -0
- package/dist/utils/toolWrapper.js.map +1 -0
- package/package.json +115 -0
- package/src/client/api.ts +1043 -0
- package/src/client/auth.ts +527 -0
- package/src/client/managers/AuthenticationManager.ts +190 -0
- package/src/client/managers/BaseManager.ts +73 -0
- package/src/client/managers/RequestManager.ts +214 -0
- package/src/client/managers/index.ts +8 -0
- package/src/index.ts +337 -0
- package/src/server.ts +7 -0
- package/src/tools/auth.ts +153 -0
- package/src/tools/comments.ts +263 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/media.ts +240 -0
- package/src/tools/pages.ts +246 -0
- package/src/tools/posts.ts +277 -0
- package/src/tools/site.ts +227 -0
- package/src/tools/taxonomies.ts +322 -0
- package/src/tools/users.ts +233 -0
- package/src/types/client.ts +304 -0
- package/src/types/index.ts +207 -0
- package/src/types/mcp.ts +247 -0
- package/src/types/wordpress.ts +491 -0
- package/src/utils/debug.ts +258 -0
- package/src/utils/error.ts +88 -0
- package/src/utils/toolWrapper.ts +105 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress API Client
|
|
3
|
+
* Handles all REST API communication with WordPress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fetch from "node-fetch";
|
|
7
|
+
import FormData from "form-data";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import type {
|
|
11
|
+
IWordPressClient,
|
|
12
|
+
WordPressClientConfig,
|
|
13
|
+
AuthConfig,
|
|
14
|
+
AuthMethod,
|
|
15
|
+
HTTPMethod,
|
|
16
|
+
RequestOptions,
|
|
17
|
+
ClientStats,
|
|
18
|
+
} from "../types/client.js";
|
|
19
|
+
import {
|
|
20
|
+
WordPressAPIError,
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
RateLimitError,
|
|
23
|
+
} from "../types/client.js";
|
|
24
|
+
import type {
|
|
25
|
+
WordPressPost,
|
|
26
|
+
WordPressPage,
|
|
27
|
+
WordPressMedia,
|
|
28
|
+
WordPressUser,
|
|
29
|
+
WordPressComment,
|
|
30
|
+
WordPressCategory,
|
|
31
|
+
WordPressTag,
|
|
32
|
+
WordPressSiteSettings,
|
|
33
|
+
WordPressApplicationPassword,
|
|
34
|
+
PostQueryParams,
|
|
35
|
+
MediaQueryParams,
|
|
36
|
+
UserQueryParams,
|
|
37
|
+
CommentQueryParams,
|
|
38
|
+
CreatePostRequest,
|
|
39
|
+
UpdatePostRequest,
|
|
40
|
+
CreatePageRequest,
|
|
41
|
+
UpdatePageRequest,
|
|
42
|
+
CreateUserRequest,
|
|
43
|
+
UpdateUserRequest,
|
|
44
|
+
CreateCommentRequest,
|
|
45
|
+
UpdateCommentRequest,
|
|
46
|
+
CreateCategoryRequest,
|
|
47
|
+
UpdateCategoryRequest,
|
|
48
|
+
CreateTagRequest,
|
|
49
|
+
UpdateTagRequest,
|
|
50
|
+
UploadMediaRequest,
|
|
51
|
+
UpdateMediaRequest,
|
|
52
|
+
} from "../types/wordpress.js";
|
|
53
|
+
import { debug, logError, startTimer } from "../utils/debug.js";
|
|
54
|
+
|
|
55
|
+
export class WordPressClient implements IWordPressClient {
|
|
56
|
+
private baseUrl: string;
|
|
57
|
+
private apiUrl: string;
|
|
58
|
+
private timeout: number;
|
|
59
|
+
private maxRetries: number;
|
|
60
|
+
private auth: AuthConfig;
|
|
61
|
+
private requestQueue: any[] = [];
|
|
62
|
+
private lastRequestTime: number = 0;
|
|
63
|
+
private requestInterval: number;
|
|
64
|
+
private authenticated: boolean = false;
|
|
65
|
+
private jwtToken: string | null = null;
|
|
66
|
+
private _stats: ClientStats;
|
|
67
|
+
|
|
68
|
+
constructor(options: Partial<WordPressClientConfig> = {}) {
|
|
69
|
+
this.baseUrl = options.baseUrl || process.env.WORDPRESS_SITE_URL || "";
|
|
70
|
+
this.apiUrl = "";
|
|
71
|
+
this.timeout =
|
|
72
|
+
options.timeout || parseInt(process.env.WORDPRESS_TIMEOUT || "30000");
|
|
73
|
+
this.maxRetries =
|
|
74
|
+
options.maxRetries || parseInt(process.env.WORDPRESS_MAX_RETRIES || "3");
|
|
75
|
+
|
|
76
|
+
// Authentication configuration
|
|
77
|
+
this.auth = options.auth || this.getAuthFromEnv();
|
|
78
|
+
|
|
79
|
+
// Rate limiting
|
|
80
|
+
this.requestInterval = 60000 / parseInt(process.env.RATE_LIMIT || "60");
|
|
81
|
+
|
|
82
|
+
// Initialize stats
|
|
83
|
+
this._stats = {
|
|
84
|
+
totalRequests: 0,
|
|
85
|
+
successfulRequests: 0,
|
|
86
|
+
failedRequests: 0,
|
|
87
|
+
averageResponseTime: 0,
|
|
88
|
+
rateLimitHits: 0,
|
|
89
|
+
authFailures: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Validate configuration
|
|
93
|
+
this.validateConfig();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get config(): WordPressClientConfig {
|
|
97
|
+
return {
|
|
98
|
+
baseUrl: this.baseUrl,
|
|
99
|
+
auth: this.auth,
|
|
100
|
+
timeout: this.timeout,
|
|
101
|
+
maxRetries: this.maxRetries,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get isAuthenticated(): boolean {
|
|
106
|
+
return this.authenticated;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get stats(): ClientStats {
|
|
110
|
+
return { ...this._stats };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getAuthFromEnv(): AuthConfig {
|
|
114
|
+
const authMethod = process.env.WORDPRESS_AUTH_METHOD as AuthMethod;
|
|
115
|
+
|
|
116
|
+
// Use explicit auth method if set
|
|
117
|
+
if (
|
|
118
|
+
authMethod === "app-password" &&
|
|
119
|
+
process.env.WORDPRESS_USERNAME &&
|
|
120
|
+
process.env.WORDPRESS_APP_PASSWORD
|
|
121
|
+
) {
|
|
122
|
+
return {
|
|
123
|
+
method: "app-password",
|
|
124
|
+
username: process.env.WORDPRESS_USERNAME,
|
|
125
|
+
appPassword: process.env.WORDPRESS_APP_PASSWORD,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Try Application Password first (fallback)
|
|
130
|
+
if (process.env.WORDPRESS_USERNAME && process.env.WORDPRESS_APP_PASSWORD) {
|
|
131
|
+
return {
|
|
132
|
+
method: "app-password",
|
|
133
|
+
username: process.env.WORDPRESS_USERNAME,
|
|
134
|
+
appPassword: process.env.WORDPRESS_APP_PASSWORD,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Try JWT
|
|
139
|
+
if (
|
|
140
|
+
process.env.WORDPRESS_JWT_SECRET &&
|
|
141
|
+
process.env.WORDPRESS_USERNAME &&
|
|
142
|
+
process.env.WORDPRESS_PASSWORD
|
|
143
|
+
) {
|
|
144
|
+
return {
|
|
145
|
+
method: "jwt",
|
|
146
|
+
secret: process.env.WORDPRESS_JWT_SECRET,
|
|
147
|
+
username: process.env.WORDPRESS_USERNAME,
|
|
148
|
+
password: process.env.WORDPRESS_PASSWORD,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Try API Key
|
|
153
|
+
if (process.env.WORDPRESS_API_KEY) {
|
|
154
|
+
return {
|
|
155
|
+
method: "api-key",
|
|
156
|
+
apiKey: process.env.WORDPRESS_API_KEY,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Try Cookie
|
|
161
|
+
if (process.env.WORDPRESS_COOKIE_NONCE) {
|
|
162
|
+
return {
|
|
163
|
+
method: "cookie",
|
|
164
|
+
nonce: process.env.WORDPRESS_COOKIE_NONCE,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Default to basic authentication
|
|
169
|
+
return {
|
|
170
|
+
method: "basic",
|
|
171
|
+
username: process.env.WORDPRESS_USERNAME || "",
|
|
172
|
+
password:
|
|
173
|
+
process.env.WORDPRESS_PASSWORD ||
|
|
174
|
+
process.env.WORDPRESS_APP_PASSWORD ||
|
|
175
|
+
"",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private validateConfig(): void {
|
|
180
|
+
if (!this.baseUrl) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"WordPress configuration is incomplete: baseUrl is required",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ensure URL doesn't end with slash and add API path
|
|
187
|
+
this.baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
188
|
+
this.apiUrl = `${this.baseUrl}/wp-json/wp/v2`;
|
|
189
|
+
|
|
190
|
+
debug.log(`WordPress API Client initialized for: ${this.apiUrl}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async initialize(): Promise<void> {
|
|
194
|
+
await this.authenticate();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async disconnect(): Promise<void> {
|
|
198
|
+
this.authenticated = false;
|
|
199
|
+
this.jwtToken = null;
|
|
200
|
+
debug.log("WordPress client disconnected");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add authentication headers to request
|
|
205
|
+
*/
|
|
206
|
+
private addAuthHeaders(headers: Record<string, string>): void {
|
|
207
|
+
const method = this.auth.method?.toLowerCase() as AuthMethod;
|
|
208
|
+
|
|
209
|
+
switch (method) {
|
|
210
|
+
case "app-password":
|
|
211
|
+
if (this.auth.username && this.auth.appPassword) {
|
|
212
|
+
const credentials = Buffer.from(
|
|
213
|
+
`${this.auth.username}:${this.auth.appPassword}`,
|
|
214
|
+
).toString("base64");
|
|
215
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case "basic":
|
|
219
|
+
if (this.auth.username && this.auth.password) {
|
|
220
|
+
const credentials = Buffer.from(
|
|
221
|
+
`${this.auth.username}:${this.auth.password}`,
|
|
222
|
+
).toString("base64");
|
|
223
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case "jwt":
|
|
228
|
+
if (this.jwtToken) {
|
|
229
|
+
headers["Authorization"] = `Bearer ${this.jwtToken}`;
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case "api-key":
|
|
234
|
+
if (this.auth.apiKey) {
|
|
235
|
+
headers["X-API-Key"] = this.auth.apiKey;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case "cookie":
|
|
240
|
+
if (this.auth.nonce) {
|
|
241
|
+
headers["X-WP-Nonce"] = this.auth.nonce;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Rate limiting implementation
|
|
249
|
+
*/
|
|
250
|
+
private async rateLimit(): Promise<void> {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
253
|
+
|
|
254
|
+
if (timeSinceLastRequest < this.requestInterval) {
|
|
255
|
+
const delay = this.requestInterval - timeSinceLastRequest;
|
|
256
|
+
await this.delay(delay);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.lastRequestTime = Date.now();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Delay utility
|
|
264
|
+
*/
|
|
265
|
+
private delay(ms: number): Promise<void> {
|
|
266
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async authenticate(): Promise<boolean> {
|
|
270
|
+
const method = this.auth.method?.toLowerCase() as AuthMethod;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
switch (method) {
|
|
274
|
+
case "app-password":
|
|
275
|
+
case "basic":
|
|
276
|
+
return await this.authenticateWithBasic();
|
|
277
|
+
case "jwt":
|
|
278
|
+
return await this.authenticateWithJWT();
|
|
279
|
+
case "cookie":
|
|
280
|
+
return await this.authenticateWithCookie();
|
|
281
|
+
case "api-key":
|
|
282
|
+
// API key auth doesn't require separate authentication step
|
|
283
|
+
this.authenticated = true;
|
|
284
|
+
return true;
|
|
285
|
+
default:
|
|
286
|
+
throw new Error(`Unsupported authentication method: ${method}`);
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this._stats.authFailures++;
|
|
290
|
+
logError(error as Error, { method });
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Authenticate using Basic/Application Password
|
|
297
|
+
*/
|
|
298
|
+
private async authenticateWithBasic(): Promise<boolean> {
|
|
299
|
+
const hasCredentials =
|
|
300
|
+
this.auth.username &&
|
|
301
|
+
(this.auth.method === "app-password"
|
|
302
|
+
? this.auth.appPassword
|
|
303
|
+
: this.auth.password);
|
|
304
|
+
|
|
305
|
+
if (!hasCredentials) {
|
|
306
|
+
const methodName =
|
|
307
|
+
this.auth.method === "app-password" ? "Application Password" : "Basic";
|
|
308
|
+
const passwordField =
|
|
309
|
+
this.auth.method === "app-password" ? "app password" : "password";
|
|
310
|
+
throw new AuthenticationError(
|
|
311
|
+
`Username and ${passwordField} are required for ${methodName} authentication`,
|
|
312
|
+
this.auth.method,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Test authentication by getting current user
|
|
318
|
+
await this.request<WordPressUser>("GET", "users/me");
|
|
319
|
+
this.authenticated = true;
|
|
320
|
+
debug.log("Basic/Application Password authentication successful");
|
|
321
|
+
return true;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new AuthenticationError(
|
|
324
|
+
`Basic authentication failed: ${(error as Error).message}`,
|
|
325
|
+
this.auth.method,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Authenticate using JWT
|
|
332
|
+
*/
|
|
333
|
+
private async authenticateWithJWT(): Promise<boolean> {
|
|
334
|
+
if (!this.auth.secret || !this.auth.username || !this.auth.password) {
|
|
335
|
+
throw new AuthenticationError(
|
|
336
|
+
"JWT secret, username, and password are required for JWT authentication",
|
|
337
|
+
this.auth.method,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(
|
|
343
|
+
`${this.baseUrl}/wp-json/jwt-auth/v1/token`,
|
|
344
|
+
{
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
},
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
username: this.auth.username,
|
|
351
|
+
password: this.auth.password,
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (!response.ok) {
|
|
357
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const data = (await response.json()) as { token: string };
|
|
361
|
+
this.jwtToken = data.token;
|
|
362
|
+
this.authenticated = true;
|
|
363
|
+
debug.log("JWT authentication successful");
|
|
364
|
+
return true;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
throw new AuthenticationError(
|
|
367
|
+
`JWT authentication failed: ${(error as Error).message}`,
|
|
368
|
+
this.auth.method,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Authenticate using Cookie
|
|
375
|
+
*/
|
|
376
|
+
private async authenticateWithCookie(): Promise<boolean> {
|
|
377
|
+
if (!this.auth.nonce) {
|
|
378
|
+
throw new AuthenticationError(
|
|
379
|
+
"Nonce is required for cookie authentication",
|
|
380
|
+
this.auth.method,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
this.authenticated = true;
|
|
384
|
+
debug.log("Cookie authentication configured");
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Make authenticated request to WordPress REST API
|
|
390
|
+
*/
|
|
391
|
+
async request<T = any>(
|
|
392
|
+
method: HTTPMethod,
|
|
393
|
+
endpoint: string,
|
|
394
|
+
data: any = null,
|
|
395
|
+
options: RequestOptions = {},
|
|
396
|
+
): Promise<T> {
|
|
397
|
+
const timer = startTimer();
|
|
398
|
+
this._stats.totalRequests++;
|
|
399
|
+
|
|
400
|
+
// Handle endpoint properly - remove leading slash if present to avoid double slashes
|
|
401
|
+
const cleanEndpoint = endpoint.replace(/^\/+/, "");
|
|
402
|
+
const url = endpoint.startsWith("http")
|
|
403
|
+
? endpoint
|
|
404
|
+
: `${this.apiUrl}/${cleanEndpoint}`;
|
|
405
|
+
|
|
406
|
+
const headers: Record<string, string> = {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
"User-Agent": "MCP-WordPress/1.0.0",
|
|
409
|
+
...options.headers,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Add authentication headers
|
|
413
|
+
this.addAuthHeaders(headers);
|
|
414
|
+
|
|
415
|
+
// Set up timeout using AbortController - use options timeout if provided
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
const requestTimeout = options.timeout || this.timeout;
|
|
418
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
419
|
+
|
|
420
|
+
const fetchOptions: any = {
|
|
421
|
+
method,
|
|
422
|
+
headers,
|
|
423
|
+
signal: controller.signal,
|
|
424
|
+
...options,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Add body for POST/PUT/PATCH requests
|
|
428
|
+
if (data && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
429
|
+
if (
|
|
430
|
+
data instanceof FormData ||
|
|
431
|
+
(data && typeof data.append === "function")
|
|
432
|
+
) {
|
|
433
|
+
// For FormData, don't set Content-Type (let fetch set it with boundary)
|
|
434
|
+
delete headers["Content-Type"];
|
|
435
|
+
fetchOptions.body = data;
|
|
436
|
+
} else if (Buffer.isBuffer(data)) {
|
|
437
|
+
// For Buffer data (manual multipart), keep Content-Type from headers
|
|
438
|
+
fetchOptions.body = data;
|
|
439
|
+
} else if (typeof data === "string") {
|
|
440
|
+
fetchOptions.body = data;
|
|
441
|
+
} else {
|
|
442
|
+
fetchOptions.body = JSON.stringify(data);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Rate limiting
|
|
447
|
+
await this.rateLimit();
|
|
448
|
+
|
|
449
|
+
let lastError: Error = new Error("Unknown error");
|
|
450
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
451
|
+
try {
|
|
452
|
+
debug.log(
|
|
453
|
+
`API Request: ${method} ${url}${attempt > 0 ? ` (attempt ${attempt + 1})` : ""}`,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const response = await fetch(url, fetchOptions);
|
|
457
|
+
clearTimeout(timeoutId);
|
|
458
|
+
|
|
459
|
+
// Handle different response types
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
const errorText = await response.text();
|
|
462
|
+
let errorMessage: string;
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const errorData = JSON.parse(errorText);
|
|
466
|
+
errorMessage =
|
|
467
|
+
errorData.message || errorData.error || `HTTP ${response.status}`;
|
|
468
|
+
} catch {
|
|
469
|
+
errorMessage =
|
|
470
|
+
errorText || `HTTP ${response.status}: ${response.statusText}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Handle rate limiting
|
|
474
|
+
if (response.status === 429) {
|
|
475
|
+
this._stats.rateLimitHits++;
|
|
476
|
+
throw new RateLimitError(errorMessage, Date.now() + 60000);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Handle permission errors specifically for uploads
|
|
480
|
+
if (
|
|
481
|
+
response.status === 403 &&
|
|
482
|
+
endpoint.includes("media") &&
|
|
483
|
+
method === "POST"
|
|
484
|
+
) {
|
|
485
|
+
throw new AuthenticationError(
|
|
486
|
+
`Media upload blocked: WordPress REST API media uploads appear to be disabled or restricted by a plugin/security policy. ` +
|
|
487
|
+
`Error: ${errorMessage}. ` +
|
|
488
|
+
`Common causes: W3 Total Cache, security plugins, or custom REST API restrictions. ` +
|
|
489
|
+
`Please check WordPress admin settings or contact your system administrator.`,
|
|
490
|
+
this.auth.method,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Handle general upload permission errors
|
|
495
|
+
if (
|
|
496
|
+
errorMessage.includes("Beiträge zu erstellen") &&
|
|
497
|
+
endpoint.includes("media")
|
|
498
|
+
) {
|
|
499
|
+
throw new AuthenticationError(
|
|
500
|
+
`WordPress REST API media upload restriction detected: ${errorMessage}. ` +
|
|
501
|
+
`This typically indicates that media uploads via REST API are disabled by WordPress configuration, ` +
|
|
502
|
+
`a security plugin (like W3 Total Cache, Borlabs Cookie), or server policy. ` +
|
|
503
|
+
`User has sufficient permissions but WordPress/plugins are blocking the upload.`,
|
|
504
|
+
this.auth.method,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
throw new WordPressAPIError(errorMessage, response.status);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Parse response
|
|
512
|
+
const responseText = await response.text();
|
|
513
|
+
if (!responseText) {
|
|
514
|
+
this._stats.successfulRequests++;
|
|
515
|
+
const duration = timer.end();
|
|
516
|
+
this.updateAverageResponseTime(duration);
|
|
517
|
+
return null as T;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const result = JSON.parse(responseText);
|
|
522
|
+
this._stats.successfulRequests++;
|
|
523
|
+
const duration = timer.end();
|
|
524
|
+
this.updateAverageResponseTime(duration);
|
|
525
|
+
return result as T;
|
|
526
|
+
} catch (parseError) {
|
|
527
|
+
// For authentication requests, malformed JSON should be an error
|
|
528
|
+
if (endpoint.includes("users/me") || endpoint.includes("jwt-auth")) {
|
|
529
|
+
throw new WordPressAPIError(
|
|
530
|
+
`Invalid JSON response: ${(parseError as Error).message}`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
this._stats.successfulRequests++;
|
|
534
|
+
const duration = timer.end();
|
|
535
|
+
this.updateAverageResponseTime(duration);
|
|
536
|
+
return responseText as T;
|
|
537
|
+
}
|
|
538
|
+
} catch (error) {
|
|
539
|
+
clearTimeout(timeoutId);
|
|
540
|
+
lastError = error as Error;
|
|
541
|
+
|
|
542
|
+
// Handle timeout errors
|
|
543
|
+
if ((error as any).name === "AbortError") {
|
|
544
|
+
lastError = new Error(`Request timeout after ${requestTimeout}ms`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Handle network errors
|
|
548
|
+
if (
|
|
549
|
+
lastError.message.includes("socket hang up") ||
|
|
550
|
+
lastError.message.includes("ECONNRESET")
|
|
551
|
+
) {
|
|
552
|
+
lastError = new Error(
|
|
553
|
+
`Network connection lost during upload: ${lastError.message}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
debug.log(
|
|
558
|
+
`Request failed (attempt ${attempt + 1}): ${lastError.message}`,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Don't retry on authentication errors, timeouts, or critical network errors
|
|
562
|
+
if (
|
|
563
|
+
lastError.message.includes("401") ||
|
|
564
|
+
lastError.message.includes("403") ||
|
|
565
|
+
lastError.message.includes("timeout") ||
|
|
566
|
+
lastError.message.includes("Network connection lost")
|
|
567
|
+
) {
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (attempt < this.maxRetries - 1) {
|
|
572
|
+
await this.delay(1000 * (attempt + 1)); // Exponential backoff
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this._stats.failedRequests++;
|
|
578
|
+
timer.end();
|
|
579
|
+
throw new WordPressAPIError(
|
|
580
|
+
`Request failed after ${this.maxRetries} attempts: ${lastError.message}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private updateAverageResponseTime(duration: number): void {
|
|
585
|
+
const totalSuccessful = this._stats.successfulRequests;
|
|
586
|
+
this._stats.averageResponseTime =
|
|
587
|
+
(this._stats.averageResponseTime * (totalSuccessful - 1) + duration) /
|
|
588
|
+
totalSuccessful;
|
|
589
|
+
this._stats.lastRequestTime = Date.now();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// HTTP method helpers
|
|
593
|
+
async get<T = any>(endpoint: string, options?: RequestOptions): Promise<T> {
|
|
594
|
+
return this.request<T>("GET", endpoint, null, options);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async post<T = any>(
|
|
598
|
+
endpoint: string,
|
|
599
|
+
data?: any,
|
|
600
|
+
options?: RequestOptions,
|
|
601
|
+
): Promise<T> {
|
|
602
|
+
return this.request<T>("POST", endpoint, data, options);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async put<T = any>(
|
|
606
|
+
endpoint: string,
|
|
607
|
+
data?: any,
|
|
608
|
+
options?: RequestOptions,
|
|
609
|
+
): Promise<T> {
|
|
610
|
+
return this.request<T>("PUT", endpoint, data, options);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async patch<T = any>(
|
|
614
|
+
endpoint: string,
|
|
615
|
+
data?: any,
|
|
616
|
+
options?: RequestOptions,
|
|
617
|
+
): Promise<T> {
|
|
618
|
+
return this.request<T>("PATCH", endpoint, data, options);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async delete<T = any>(
|
|
622
|
+
endpoint: string,
|
|
623
|
+
options?: RequestOptions,
|
|
624
|
+
): Promise<T> {
|
|
625
|
+
return this.request<T>("DELETE", endpoint, null, options);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// WordPress API Methods
|
|
629
|
+
|
|
630
|
+
// Posts
|
|
631
|
+
async getPosts(params?: PostQueryParams): Promise<WordPressPost[]> {
|
|
632
|
+
const queryString = params
|
|
633
|
+
? "?" + new URLSearchParams(params as any).toString()
|
|
634
|
+
: "";
|
|
635
|
+
return this.get<WordPressPost[]>(`posts${queryString}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async getPost(
|
|
639
|
+
id: number,
|
|
640
|
+
context: "view" | "embed" | "edit" = "view",
|
|
641
|
+
): Promise<WordPressPost> {
|
|
642
|
+
return this.get<WordPressPost>(`posts/${id}?context=${context}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async createPost(data: CreatePostRequest): Promise<WordPressPost> {
|
|
646
|
+
return this.post<WordPressPost>("posts", data);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async updatePost(data: UpdatePostRequest): Promise<WordPressPost> {
|
|
650
|
+
const { id, ...updateData } = data;
|
|
651
|
+
return this.put<WordPressPost>(`posts/${id}`, updateData);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async deletePost(
|
|
655
|
+
id: number,
|
|
656
|
+
force = false,
|
|
657
|
+
): Promise<{ deleted: boolean; previous?: WordPressPost }> {
|
|
658
|
+
return this.delete(`posts/${id}?force=${force}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async getPostRevisions(id: number): Promise<WordPressPost[]> {
|
|
662
|
+
return this.get<WordPressPost[]>(`posts/${id}/revisions`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Pages
|
|
666
|
+
async getPages(params?: PostQueryParams): Promise<WordPressPage[]> {
|
|
667
|
+
const queryString = params
|
|
668
|
+
? "?" + new URLSearchParams(params as any).toString()
|
|
669
|
+
: "";
|
|
670
|
+
return this.get<WordPressPage[]>(`pages${queryString}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async getPage(
|
|
674
|
+
id: number,
|
|
675
|
+
context: "view" | "embed" | "edit" = "view",
|
|
676
|
+
): Promise<WordPressPage> {
|
|
677
|
+
return this.get<WordPressPage>(`pages/${id}?context=${context}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async createPage(data: CreatePageRequest): Promise<WordPressPage> {
|
|
681
|
+
return this.post<WordPressPage>("pages", data);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async updatePage(data: UpdatePageRequest): Promise<WordPressPage> {
|
|
685
|
+
const { id, ...updateData } = data;
|
|
686
|
+
return this.put<WordPressPage>(`pages/${id}`, updateData);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async deletePage(
|
|
690
|
+
id: number,
|
|
691
|
+
force = false,
|
|
692
|
+
): Promise<{ deleted: boolean; previous?: WordPressPage }> {
|
|
693
|
+
return this.delete(`pages/${id}?force=${force}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async getPageRevisions(id: number): Promise<WordPressPage[]> {
|
|
697
|
+
return this.get<WordPressPage[]>(`pages/${id}/revisions`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Media
|
|
701
|
+
async getMedia(params?: MediaQueryParams): Promise<WordPressMedia[]> {
|
|
702
|
+
const queryString = params
|
|
703
|
+
? "?" + new URLSearchParams(params as any).toString()
|
|
704
|
+
: "";
|
|
705
|
+
return this.get<WordPressMedia[]>(`media${queryString}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async getMediaItem(
|
|
709
|
+
id: number,
|
|
710
|
+
context: "view" | "embed" | "edit" = "view",
|
|
711
|
+
): Promise<WordPressMedia> {
|
|
712
|
+
return this.get<WordPressMedia>(`media/${id}?context=${context}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async uploadMedia(data: UploadMediaRequest): Promise<WordPressMedia> {
|
|
716
|
+
if (!fs.existsSync(data.file_path)) {
|
|
717
|
+
throw new Error(`File not found: ${data.file_path}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const stats = fs.statSync(data.file_path);
|
|
721
|
+
const filename = data.title || path.basename(data.file_path);
|
|
722
|
+
const fileBuffer = fs.readFileSync(data.file_path);
|
|
723
|
+
|
|
724
|
+
// Check if file is too large (WordPress default is 2MB for most installs)
|
|
725
|
+
const maxSize = 10 * 1024 * 1024; // 10MB reasonable limit
|
|
726
|
+
if (stats.size > maxSize) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
`File too large: ${(stats.size / 1024 / 1024).toFixed(2)}MB. Maximum allowed: ${maxSize / 1024 / 1024}MB`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
debug.log(
|
|
733
|
+
`Uploading file: ${filename} (${(stats.size / 1024).toFixed(2)}KB)`,
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
return this.uploadFile(
|
|
737
|
+
fileBuffer,
|
|
738
|
+
filename,
|
|
739
|
+
this.getMimeType(data.file_path),
|
|
740
|
+
data,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async uploadFile(
|
|
745
|
+
fileData: Buffer,
|
|
746
|
+
filename: string,
|
|
747
|
+
mimeType: string,
|
|
748
|
+
meta: Partial<UploadMediaRequest> = {},
|
|
749
|
+
options?: RequestOptions,
|
|
750
|
+
): Promise<WordPressMedia> {
|
|
751
|
+
debug.log(`Uploading file: ${filename} (${fileData.length} bytes)`);
|
|
752
|
+
|
|
753
|
+
// Use FormData but with correct configuration for node-fetch
|
|
754
|
+
const formData = new FormData();
|
|
755
|
+
formData.setMaxListeners(20);
|
|
756
|
+
|
|
757
|
+
// Add file with correct options
|
|
758
|
+
formData.append("file", fileData, {
|
|
759
|
+
filename,
|
|
760
|
+
contentType: mimeType,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Add metadata
|
|
764
|
+
if (meta.title) formData.append("title", meta.title);
|
|
765
|
+
if (meta.alt_text) formData.append("alt_text", meta.alt_text);
|
|
766
|
+
if (meta.caption) formData.append("caption", meta.caption);
|
|
767
|
+
if (meta.description) formData.append("description", meta.description);
|
|
768
|
+
if (meta.post) formData.append("post", meta.post.toString());
|
|
769
|
+
|
|
770
|
+
// Use longer timeout for file uploads
|
|
771
|
+
const uploadTimeout =
|
|
772
|
+
options?.timeout !== undefined ? options.timeout : 600000; // 10 minutes default
|
|
773
|
+
const uploadOptions: RequestOptions = {
|
|
774
|
+
...options,
|
|
775
|
+
timeout: uploadTimeout,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
debug.log(`Upload prepared with FormData, timeout: ${uploadTimeout}ms`);
|
|
779
|
+
|
|
780
|
+
// Use the regular post method which handles FormData correctly
|
|
781
|
+
return this.post<WordPressMedia>("media", formData, uploadOptions);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async updateMedia(data: UpdateMediaRequest): Promise<WordPressMedia> {
|
|
785
|
+
const { id, ...updateData } = data;
|
|
786
|
+
return this.put<WordPressMedia>(`media/${id}`, updateData);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async deleteMedia(
|
|
790
|
+
id: number,
|
|
791
|
+
force = false,
|
|
792
|
+
): Promise<{ deleted: boolean; previous?: WordPressMedia }> {
|
|
793
|
+
return this.delete(`media/${id}?force=${force}`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Users
|
|
797
|
+
async getUsers(params?: UserQueryParams): Promise<WordPressUser[]> {
|
|
798
|
+
const queryString = params
|
|
799
|
+
? "?" + new URLSearchParams(params as any).toString()
|
|
800
|
+
: "";
|
|
801
|
+
return this.get<WordPressUser[]>(`users${queryString}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async getUser(
|
|
805
|
+
id: number | "me",
|
|
806
|
+
context: "view" | "embed" | "edit" = "view",
|
|
807
|
+
): Promise<WordPressUser> {
|
|
808
|
+
return this.get<WordPressUser>(`users/${id}?context=${context}`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async createUser(data: CreateUserRequest): Promise<WordPressUser> {
|
|
812
|
+
return this.post<WordPressUser>("users", data);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async updateUser(data: UpdateUserRequest): Promise<WordPressUser> {
|
|
816
|
+
const { id, ...updateData } = data;
|
|
817
|
+
return this.put<WordPressUser>(`users/${id}`, updateData);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async deleteUser(
|
|
821
|
+
id: number,
|
|
822
|
+
reassign?: number,
|
|
823
|
+
): Promise<{ deleted: boolean; previous?: WordPressUser }> {
|
|
824
|
+
const params = reassign
|
|
825
|
+
? `?reassign=${reassign}&force=true`
|
|
826
|
+
: "?force=true";
|
|
827
|
+
return this.delete(`users/${id}${params}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async getCurrentUser(): Promise<WordPressUser> {
|
|
831
|
+
return this.getUser("me");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Comments
|
|
835
|
+
async getComments(params?: CommentQueryParams): Promise<WordPressComment[]> {
|
|
836
|
+
const queryString = params
|
|
837
|
+
? "?" + new URLSearchParams(params as any).toString()
|
|
838
|
+
: "";
|
|
839
|
+
return this.get<WordPressComment[]>(`comments${queryString}`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async getComment(
|
|
843
|
+
id: number,
|
|
844
|
+
context: "view" | "embed" | "edit" = "view",
|
|
845
|
+
): Promise<WordPressComment> {
|
|
846
|
+
return this.get<WordPressComment>(`comments/${id}?context=${context}`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async createComment(data: CreateCommentRequest): Promise<WordPressComment> {
|
|
850
|
+
return this.post<WordPressComment>("comments", data);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async updateComment(data: UpdateCommentRequest): Promise<WordPressComment> {
|
|
854
|
+
const { id, ...updateData } = data;
|
|
855
|
+
return this.put<WordPressComment>(`comments/${id}`, updateData);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async deleteComment(
|
|
859
|
+
id: number,
|
|
860
|
+
force = false,
|
|
861
|
+
): Promise<{ deleted: boolean; previous?: WordPressComment }> {
|
|
862
|
+
return this.delete(`comments/${id}?force=${force}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async approveComment(id: number): Promise<WordPressComment> {
|
|
866
|
+
return this.put<WordPressComment>(`comments/${id}`, { status: "approved" });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async rejectComment(id: number): Promise<WordPressComment> {
|
|
870
|
+
return this.put<WordPressComment>(`comments/${id}`, {
|
|
871
|
+
status: "unapproved",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async spamComment(id: number): Promise<WordPressComment> {
|
|
876
|
+
return this.put<WordPressComment>(`comments/${id}`, { status: "spam" });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Taxonomies
|
|
880
|
+
async getCategories(params?: any): Promise<WordPressCategory[]> {
|
|
881
|
+
const queryString = params
|
|
882
|
+
? "?" + new URLSearchParams(params).toString()
|
|
883
|
+
: "";
|
|
884
|
+
return this.get<WordPressCategory[]>(`categories${queryString}`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async getCategory(id: number): Promise<WordPressCategory> {
|
|
888
|
+
return this.get<WordPressCategory>(`categories/${id}`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async createCategory(
|
|
892
|
+
data: CreateCategoryRequest,
|
|
893
|
+
): Promise<WordPressCategory> {
|
|
894
|
+
return this.post<WordPressCategory>("categories", data);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async updateCategory(
|
|
898
|
+
data: UpdateCategoryRequest,
|
|
899
|
+
): Promise<WordPressCategory> {
|
|
900
|
+
const { id, ...updateData } = data;
|
|
901
|
+
return this.put<WordPressCategory>(`categories/${id}`, updateData);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async deleteCategory(
|
|
905
|
+
id: number,
|
|
906
|
+
force = false,
|
|
907
|
+
): Promise<{ deleted: boolean; previous?: WordPressCategory }> {
|
|
908
|
+
return this.delete(`categories/${id}?force=${force}`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async getTags(params?: any): Promise<WordPressTag[]> {
|
|
912
|
+
const queryString = params
|
|
913
|
+
? "?" + new URLSearchParams(params).toString()
|
|
914
|
+
: "";
|
|
915
|
+
return this.get<WordPressTag[]>(`tags${queryString}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async getTag(id: number): Promise<WordPressTag> {
|
|
919
|
+
return this.get<WordPressTag>(`tags/${id}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async createTag(data: CreateTagRequest): Promise<WordPressTag> {
|
|
923
|
+
return this.post<WordPressTag>("tags", data);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async updateTag(data: UpdateTagRequest): Promise<WordPressTag> {
|
|
927
|
+
const { id, ...updateData } = data;
|
|
928
|
+
return this.put<WordPressTag>(`tags/${id}`, updateData);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async deleteTag(
|
|
932
|
+
id: number,
|
|
933
|
+
force = false,
|
|
934
|
+
): Promise<{ deleted: boolean; previous?: WordPressTag }> {
|
|
935
|
+
return this.delete(`tags/${id}?force=${force}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Site Management
|
|
939
|
+
async getSiteSettings(): Promise<WordPressSiteSettings> {
|
|
940
|
+
return this.get<WordPressSiteSettings>("settings");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async updateSiteSettings(
|
|
944
|
+
settings: Partial<WordPressSiteSettings>,
|
|
945
|
+
): Promise<WordPressSiteSettings> {
|
|
946
|
+
return this.post<WordPressSiteSettings>("settings", settings);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async getSiteInfo(): Promise<any> {
|
|
950
|
+
return this.get("");
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Application Passwords
|
|
954
|
+
async getApplicationPasswords(
|
|
955
|
+
userId: number | "me" = "me",
|
|
956
|
+
): Promise<WordPressApplicationPassword[]> {
|
|
957
|
+
return this.get<WordPressApplicationPassword[]>(
|
|
958
|
+
`users/${userId}/application-passwords`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async createApplicationPassword(
|
|
963
|
+
userId: number | "me",
|
|
964
|
+
name: string,
|
|
965
|
+
appId?: string,
|
|
966
|
+
): Promise<WordPressApplicationPassword> {
|
|
967
|
+
const data: any = { name };
|
|
968
|
+
if (appId) data.app_id = appId;
|
|
969
|
+
return this.post<WordPressApplicationPassword>(
|
|
970
|
+
`users/${userId}/application-passwords`,
|
|
971
|
+
data,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async deleteApplicationPassword(
|
|
976
|
+
userId: number | "me",
|
|
977
|
+
uuid: string,
|
|
978
|
+
): Promise<{ deleted: boolean }> {
|
|
979
|
+
return this.delete(`users/${userId}/application-passwords/${uuid}`);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Search
|
|
983
|
+
async search(
|
|
984
|
+
query: string,
|
|
985
|
+
types?: string[],
|
|
986
|
+
subtype?: string,
|
|
987
|
+
): Promise<any[]> {
|
|
988
|
+
const params = new URLSearchParams({ search: query });
|
|
989
|
+
if (types) params.append("type", types.join(","));
|
|
990
|
+
if (subtype) params.append("subtype", subtype);
|
|
991
|
+
|
|
992
|
+
return this.get<any[]>(`search?${params.toString()}`);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Utility Methods
|
|
996
|
+
async ping(): Promise<boolean> {
|
|
997
|
+
try {
|
|
998
|
+
await this.get("");
|
|
999
|
+
return true;
|
|
1000
|
+
} catch {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async getServerInfo(): Promise<Record<string, any>> {
|
|
1006
|
+
return this.get("");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
validateEndpoint(endpoint: string): boolean {
|
|
1010
|
+
return /^[a-zA-Z0-9\/\-_]+$/.test(endpoint);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
buildUrl(endpoint: string, params?: Record<string, any>): string {
|
|
1014
|
+
const url = `${this.apiUrl}/${endpoint.replace(/^\/+/, "")}`;
|
|
1015
|
+
if (params) {
|
|
1016
|
+
const searchParams = new URLSearchParams(params);
|
|
1017
|
+
return `${url}?${searchParams.toString()}`;
|
|
1018
|
+
}
|
|
1019
|
+
return url;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private getMimeType(filePath: string): string {
|
|
1023
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1024
|
+
const mimeTypes: Record<string, string> = {
|
|
1025
|
+
".jpg": "image/jpeg",
|
|
1026
|
+
".jpeg": "image/jpeg",
|
|
1027
|
+
".png": "image/png",
|
|
1028
|
+
".gif": "image/gif",
|
|
1029
|
+
".webp": "image/webp",
|
|
1030
|
+
".svg": "image/svg+xml",
|
|
1031
|
+
".pdf": "application/pdf",
|
|
1032
|
+
".doc": "application/msword",
|
|
1033
|
+
".docx":
|
|
1034
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1035
|
+
".txt": "text/plain",
|
|
1036
|
+
".mp4": "video/mp4",
|
|
1037
|
+
".mp3": "audio/mpeg",
|
|
1038
|
+
".wav": "audio/wav",
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
1042
|
+
}
|
|
1043
|
+
}
|