@marvalt/digivalt-core 0.1.6 → 0.2.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/CHANGELOG.md +13 -0
- package/README.md +32 -4
- package/bin/init.cjs +42 -7
- package/dist/config.cjs +520 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.ts +307 -0
- package/dist/config.esm.js +502 -0
- package/dist/config.esm.js.map +1 -0
- package/dist/generators.cjs +2481 -0
- package/dist/generators.cjs.map +1 -0
- package/dist/generators.d.ts +19 -0
- package/dist/generators.esm.js +2475 -0
- package/dist/generators.esm.js.map +1 -0
- package/dist/index.cjs +18 -7955
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +18 -1196
- package/dist/index.esm.js +13 -7929
- package/dist/index.esm.js.map +1 -1
- package/dist/runtime/env.d.ts +4 -0
- package/dist/runtime/lazy.d.ts +1 -0
- package/dist/services/cf-wp-webhook.d.ts +2 -0
- package/dist/services/gravityForms.d.ts +3 -0
- package/dist/services/mautic.d.ts +5 -1
- package/dist/services/suitecrm.d.ts +2 -0
- package/dist/services.cjs +1339 -0
- package/dist/services.cjs.map +1 -0
- package/dist/services.d.ts +432 -0
- package/dist/services.esm.js +1322 -0
- package/dist/services.esm.js.map +1 -0
- package/dist/static/index.d.ts +4 -0
- package/dist/static.cjs +997 -0
- package/dist/static.cjs.map +1 -0
- package/dist/static.d.ts +410 -0
- package/dist/static.esm.js +962 -0
- package/dist/static.esm.js.map +1 -0
- package/dist/types.cjs +3 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.esm.js +2 -0
- package/dist/types.esm.js.map +1 -0
- package/package.json +30 -2
- package/template/DIGIVALT_SETUP.md +62 -0
- package/template/scripts/deploy-secrets.js +55 -23
- package/template/scripts/generate.ts +19 -0
|
@@ -0,0 +1,2481 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var dotenv = require('dotenv');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var sadapter = require('@marvalt/sadapter');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @license GPL-3.0-or-later
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the MarVAlt Open SDK.
|
|
12
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
13
|
+
*
|
|
14
|
+
* This program is free software: you can redistribute it and/or modify
|
|
15
|
+
* it under the terms of the GNU General Public License as published by
|
|
16
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
17
|
+
* (at your option) any later version.
|
|
18
|
+
*
|
|
19
|
+
* This program is distributed in the hope that it will be useful,
|
|
20
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
21
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
22
|
+
* See the GNU General Public License for more details.
|
|
23
|
+
*/
|
|
24
|
+
class WordPressClient {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
async makeRequest(endpoint, params = {}, options = {}) {
|
|
29
|
+
const baseUrl = this.getBaseUrl();
|
|
30
|
+
const queryString = new URLSearchParams();
|
|
31
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
32
|
+
if (value !== undefined && value !== null) {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
value.forEach(v => queryString.append(key, v.toString()));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
queryString.append(key, value.toString());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Construct URL based on auth mode
|
|
42
|
+
let url;
|
|
43
|
+
if (this.config.authMode === 'cloudflare_proxy') {
|
|
44
|
+
// For proxy modes, pass the full REST path including /wp-json as query parameter
|
|
45
|
+
const fullEndpoint = `/wp-json${endpoint}?${queryString.toString()}`;
|
|
46
|
+
url = `${baseUrl}?endpoint=${encodeURIComponent(fullEndpoint)}`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// For direct mode, construct the full WordPress API URL
|
|
50
|
+
url = `${baseUrl}/wp-json${endpoint}?${queryString.toString()}`;
|
|
51
|
+
}
|
|
52
|
+
const headers = {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
};
|
|
55
|
+
// Add authentication based on mode
|
|
56
|
+
if (this.config.authMode === 'cloudflare_proxy') ;
|
|
57
|
+
else {
|
|
58
|
+
// Direct mode - use basic auth
|
|
59
|
+
if (this.config.username && this.config.password) {
|
|
60
|
+
const credentials = btoa(`${this.config.username}:${this.config.password}`);
|
|
61
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
62
|
+
}
|
|
63
|
+
// Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
|
|
64
|
+
if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
|
|
65
|
+
headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
|
|
66
|
+
headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
|
|
67
|
+
console.log('🔐 Added CF Access Service Token headers to WordPress request');
|
|
68
|
+
console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
|
|
69
|
+
console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
|
|
73
|
+
console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
|
|
74
|
+
console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log('🔍 WordPress API Request Debug:', {
|
|
78
|
+
url,
|
|
79
|
+
method: options.method || 'GET',
|
|
80
|
+
authMode: this.config.authMode,
|
|
81
|
+
headers: Object.keys(headers),
|
|
82
|
+
hasBasicAuth: !!headers.Authorization,
|
|
83
|
+
hasCfAccess: !!(headers['CF-Access-Client-Id'] && headers['CF-Access-Client-Secret']),
|
|
84
|
+
});
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: options.method || 'GET',
|
|
87
|
+
headers,
|
|
88
|
+
body: options.body,
|
|
89
|
+
signal: AbortSignal.timeout(this.config.timeout || 30000),
|
|
90
|
+
...options,
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
console.error('❌ WordPress API Error:', {
|
|
94
|
+
status: response.status,
|
|
95
|
+
statusText: response.statusText,
|
|
96
|
+
url,
|
|
97
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
98
|
+
});
|
|
99
|
+
throw new Error(`WordPress API request failed: ${response.status} ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
const responseText = await response.text();
|
|
102
|
+
console.log('🔍 WordPress API Response:', {
|
|
103
|
+
status: response.status,
|
|
104
|
+
responseLength: responseText.length,
|
|
105
|
+
responsePreview: responseText.substring(0, 200),
|
|
106
|
+
});
|
|
107
|
+
if (!responseText) {
|
|
108
|
+
// Return empty array for empty responses (common for endpoints with no data)
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(responseText);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
console.error('❌ JSON Parse Error:', {
|
|
117
|
+
error: message,
|
|
118
|
+
responseText: responseText.substring(0, 500),
|
|
119
|
+
});
|
|
120
|
+
throw new Error(`Failed to parse JSON response: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
getBaseUrl() {
|
|
124
|
+
switch (this.config.authMode) {
|
|
125
|
+
case 'cloudflare_proxy':
|
|
126
|
+
return this.config.cloudflareWorkerUrl || '';
|
|
127
|
+
default:
|
|
128
|
+
return this.config.apiUrl || '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Fetch posts with automatic pagination support
|
|
133
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
134
|
+
* @returns All posts up to the limit, fetched across multiple pages if needed
|
|
135
|
+
*/
|
|
136
|
+
async getPosts(params = {}) {
|
|
137
|
+
return this.fetchPaginated('/wp/v2/posts', params);
|
|
138
|
+
}
|
|
139
|
+
async getPost(id) {
|
|
140
|
+
return this.makeRequest(`/wp/v2/posts/${id}`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fetch pages with automatic pagination support
|
|
144
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
145
|
+
* @returns All pages up to the limit, fetched across multiple pages if needed
|
|
146
|
+
*/
|
|
147
|
+
async getPages(params = {}) {
|
|
148
|
+
return this.fetchPaginated('/wp/v2/pages', params);
|
|
149
|
+
}
|
|
150
|
+
async getPage(id) {
|
|
151
|
+
return this.makeRequest(`/wp/v2/pages/${id}`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Fetch media with automatic pagination support
|
|
155
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
156
|
+
* @returns All media items up to the limit, fetched across multiple pages if needed
|
|
157
|
+
*/
|
|
158
|
+
async getMedia(params = {}) {
|
|
159
|
+
return this.fetchPaginated('/wp/v2/media', params);
|
|
160
|
+
}
|
|
161
|
+
async getMediaItem(id) {
|
|
162
|
+
return this.makeRequest(`/wp/v2/media/${id}`);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Fetch categories with automatic pagination support
|
|
166
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
167
|
+
* @returns All categories up to the limit, fetched across multiple pages if needed
|
|
168
|
+
*/
|
|
169
|
+
async getCategories(params = {}) {
|
|
170
|
+
return this.fetchPaginated('/wp/v2/categories', params);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Fetch tags with automatic pagination support
|
|
174
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
175
|
+
* @returns All tags up to the limit, fetched across multiple pages if needed
|
|
176
|
+
*/
|
|
177
|
+
async getTags(params = {}) {
|
|
178
|
+
return this.fetchPaginated('/wp/v2/tags', params);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Fetch data with automatic pagination
|
|
182
|
+
* Handles fetching all items up to the requested limit by making multiple API calls if needed
|
|
183
|
+
* @param endpoint The API endpoint to fetch from
|
|
184
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
185
|
+
* @returns Array of all items fetched across all pages
|
|
186
|
+
*/
|
|
187
|
+
async fetchPaginated(endpoint, params = {}) {
|
|
188
|
+
const requestedTotal = params.per_page || 100;
|
|
189
|
+
const itemsPerPage = Math.min(requestedTotal, 100); // WordPress max is typically 100
|
|
190
|
+
const maxPages = Math.ceil(requestedTotal / itemsPerPage);
|
|
191
|
+
let allItems = [];
|
|
192
|
+
let currentPage = 1;
|
|
193
|
+
let hasMorePages = true;
|
|
194
|
+
console.log(`🔄 Fetching ${endpoint} with pagination: ${requestedTotal} items requested (${maxPages} pages of ${itemsPerPage})`);
|
|
195
|
+
while (hasMorePages && currentPage <= maxPages && allItems.length < requestedTotal) {
|
|
196
|
+
const pageParams = {
|
|
197
|
+
...params,
|
|
198
|
+
per_page: itemsPerPage,
|
|
199
|
+
page: currentPage,
|
|
200
|
+
};
|
|
201
|
+
console.log(`📄 Fetching page ${currentPage}/${maxPages} for ${endpoint}...`);
|
|
202
|
+
try {
|
|
203
|
+
const pageData = await this.makeRequest(endpoint, pageParams);
|
|
204
|
+
if (!pageData || pageData.length === 0) {
|
|
205
|
+
console.log(`✓ No more data on page ${currentPage}, stopping pagination`);
|
|
206
|
+
hasMorePages = false;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
allItems = allItems.concat(pageData);
|
|
210
|
+
console.log(`✓ Page ${currentPage}: fetched ${pageData.length} items (total: ${allItems.length})`);
|
|
211
|
+
// Stop if we got fewer items than requested per page (last page)
|
|
212
|
+
if (pageData.length < itemsPerPage) {
|
|
213
|
+
console.log(`✓ Reached last page (partial results)`);
|
|
214
|
+
hasMorePages = false;
|
|
215
|
+
}
|
|
216
|
+
currentPage++;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error(`❌ Error fetching page ${currentPage}:`, error);
|
|
220
|
+
// Stop pagination on error but return what we have so far
|
|
221
|
+
hasMorePages = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
console.log(`✅ Pagination complete for ${endpoint}: ${allItems.length} total items fetched`);
|
|
225
|
+
// Trim to exact requested amount if we fetched more
|
|
226
|
+
return allItems.slice(0, requestedTotal);
|
|
227
|
+
}
|
|
228
|
+
// Gravity Forms endpoints
|
|
229
|
+
async getGravityForms() {
|
|
230
|
+
return this.makeRequest('/gf-api/v1/forms');
|
|
231
|
+
}
|
|
232
|
+
async getGravityForm(formId) {
|
|
233
|
+
return this.makeRequest(`/gf-api/v1/forms/${formId}/config`);
|
|
234
|
+
}
|
|
235
|
+
async submitGravityForm(formId, entry) {
|
|
236
|
+
return this.makeRequest(`/gf-api/v1/forms/${formId}/submit`, {}, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
body: JSON.stringify(entry),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Static data endpoints
|
|
242
|
+
async getStaticDataNonce() {
|
|
243
|
+
return this.makeRequest('/static-data/v1/nonce');
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Fetch a custom post type with automatic pagination support
|
|
247
|
+
* @param postType The custom post type slug (e.g., 'chapter-member', 'events')
|
|
248
|
+
* @param params Query parameters including per_page for total items to fetch
|
|
249
|
+
* @returns All items of the custom post type up to the limit
|
|
250
|
+
*/
|
|
251
|
+
async getCustomPostType(postType, params = {}) {
|
|
252
|
+
return this.fetchPaginated(`/wp/v2/${postType}`, params);
|
|
253
|
+
}
|
|
254
|
+
async getAllData(params = {}) {
|
|
255
|
+
const [posts, pages, media, categories, tags] = await Promise.all([
|
|
256
|
+
this.getPosts(params),
|
|
257
|
+
this.getPages(params),
|
|
258
|
+
this.getMedia(params),
|
|
259
|
+
this.getCategories(params),
|
|
260
|
+
this.getTags(params),
|
|
261
|
+
]);
|
|
262
|
+
return { posts, pages, media, categories, tags };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Fetch blocks for a specific page from the custom blocks endpoint
|
|
266
|
+
* @param pageId Page ID
|
|
267
|
+
* @returns Blocks data from /wp-custom/v1/pages/{id}/blocks
|
|
268
|
+
*/
|
|
269
|
+
async getPageBlocks(pageId) {
|
|
270
|
+
return this.makeRequest(`/wp-custom/v1/pages/${pageId}/blocks`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Fetch WordPress Settings from custom endpoint
|
|
274
|
+
* @returns Settings including front page information and site branding
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* Fetch header template part blocks from custom endpoint
|
|
278
|
+
* @param slug Template part slug (default: 'header')
|
|
279
|
+
* @param area Template part area (default: 'header')
|
|
280
|
+
* @returns Header blocks data
|
|
281
|
+
*/
|
|
282
|
+
async getHeader(slug = 'header', area = 'header') {
|
|
283
|
+
return this.makeRequest(`/wp-custom/v1/header`, { slug, area });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Fetch all navigation menus
|
|
287
|
+
* @returns Array of menu objects
|
|
288
|
+
*/
|
|
289
|
+
async getMenus() {
|
|
290
|
+
return this.makeRequest('/wp-custom/v1/menus');
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fetch a specific navigation menu with its items
|
|
294
|
+
* Handles both menu term IDs and wp_navigation post IDs
|
|
295
|
+
* @param menuId Menu ID (can be menu term ID or wp_navigation post ID)
|
|
296
|
+
* @returns Menu data with hierarchical items
|
|
297
|
+
*/
|
|
298
|
+
async getMenu(menuId) {
|
|
299
|
+
console.log(`🌐 [WordPressClient] Fetching menu ID ${menuId}...`);
|
|
300
|
+
try {
|
|
301
|
+
// First try the custom endpoint (handles both menu terms and wp_navigation posts)
|
|
302
|
+
console.log(` → Trying endpoint: /wp-custom/v1/menus/${menuId}`);
|
|
303
|
+
const result = await this.makeRequest(`/wp-custom/v1/menus/${menuId}`);
|
|
304
|
+
console.log(` ✅ Successfully fetched menu ID ${menuId} as menu term: ${result.name}`);
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const errorStatus = error.status || (error.response?.status) || 'unknown';
|
|
309
|
+
const errorMessage = error.message || 'Unknown error';
|
|
310
|
+
console.log(` ❌ Menu term fetch failed (status: ${errorStatus}): ${errorMessage}`);
|
|
311
|
+
// If 404, try fetching as wp_navigation post
|
|
312
|
+
if (errorStatus === 404 || errorMessage?.includes('404')) {
|
|
313
|
+
console.log(` → Menu ID ${menuId} not found as menu term, trying as wp_navigation post...`);
|
|
314
|
+
// Try custom endpoint first
|
|
315
|
+
try {
|
|
316
|
+
console.log(` → Trying navigation post endpoint: /wp-custom/v1/navigation/${menuId}`);
|
|
317
|
+
const navPost = await this.makeRequest(`/wp-custom/v1/navigation/${menuId}`);
|
|
318
|
+
console.log(` ✅ Navigation post fetched:`, {
|
|
319
|
+
id: navPost.id,
|
|
320
|
+
hasMenu: !!navPost.menu,
|
|
321
|
+
menuTermId: navPost.menu_term_id,
|
|
322
|
+
});
|
|
323
|
+
// If navigation post has menu data (either from menu_term_id or extracted from blocks)
|
|
324
|
+
if (navPost.menu) {
|
|
325
|
+
console.log(` ✅ Returning menu from navigation post: ${navPost.menu.name} (${navPost.menu.items?.length || 0} items)`);
|
|
326
|
+
return navPost.menu;
|
|
327
|
+
}
|
|
328
|
+
// If navigation post has a menu_term_id but no menu data, fetch that menu
|
|
329
|
+
if (navPost.menu_term_id) {
|
|
330
|
+
console.log(` → Navigation post has menu_term_id ${navPost.menu_term_id}, fetching menu...`);
|
|
331
|
+
const menu = await this.makeRequest(`/wp-custom/v1/menus/${navPost.menu_term_id}`);
|
|
332
|
+
console.log(` ✅ Fetched menu from menu_term_id ${navPost.menu_term_id}: ${menu.name}`);
|
|
333
|
+
return menu;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
console.warn(` ⚠️ Navigation post ${menuId} has no menu_term_id and no menu data extracted from blocks`);
|
|
337
|
+
throw new Error(`Navigation post ${menuId} has no menu_term_id and no menu data`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (navError) {
|
|
341
|
+
const navErrorStatus = navError.status || (navError.response?.status) || 'unknown';
|
|
342
|
+
const navErrorMessage = navError.message || 'Unknown error';
|
|
343
|
+
console.log(` ❌ Navigation post fetch failed (status: ${navErrorStatus}): ${navErrorMessage}`);
|
|
344
|
+
// If custom endpoint fails, try WordPress native REST API (WordPress 6.3+)
|
|
345
|
+
if (navErrorStatus === 404 || navErrorMessage?.includes('404')) {
|
|
346
|
+
console.log(` → Custom navigation endpoint not found, trying WordPress native REST API...`);
|
|
347
|
+
try {
|
|
348
|
+
console.log(` → Trying WordPress native REST API: /wp/v2/navigation/${menuId}`);
|
|
349
|
+
// WordPress 6.3+ has native REST API for wp_navigation posts
|
|
350
|
+
const navPost = await this.makeRequest(`/wp/v2/navigation/${menuId}`);
|
|
351
|
+
console.log(` ✅ WordPress native navigation post fetched:`, {
|
|
352
|
+
id: navPost.id,
|
|
353
|
+
title: navPost.title?.rendered,
|
|
354
|
+
hasContent: !!navPost.content?.raw,
|
|
355
|
+
});
|
|
356
|
+
// Parse blocks from content to find menu reference
|
|
357
|
+
// The content.raw contains the block JSON
|
|
358
|
+
if (navPost.content?.raw) {
|
|
359
|
+
try {
|
|
360
|
+
const blocks = JSON.parse(navPost.content.raw);
|
|
361
|
+
console.log(` → Parsed ${Array.isArray(blocks) ? blocks.length : 'unknown'} blocks from navigation post`);
|
|
362
|
+
// Look for menu reference in blocks (this would need to be done server-side)
|
|
363
|
+
// For now, throw error to indicate menu extraction requires server-side processing
|
|
364
|
+
console.log(`ℹ️ Found wp_navigation post ${menuId}, but menu extraction requires server-side processing`);
|
|
365
|
+
throw new Error(`Menu extraction from wp_navigation post ${menuId} requires server-side processing`);
|
|
366
|
+
}
|
|
367
|
+
catch (parseError) {
|
|
368
|
+
console.log(`⚠️ Could not parse navigation post content`);
|
|
369
|
+
throw new Error(`Could not parse navigation post content for menu ID ${menuId}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
throw new Error(`wp_navigation post ${menuId} has no content to parse`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (wpError) {
|
|
377
|
+
const wpErrorStatus = wpError.status || (wpError.response?.status) || 'unknown';
|
|
378
|
+
const wpErrorMessage = wpError.message || 'Unknown error';
|
|
379
|
+
console.error(` ❌ WordPress native REST API also failed (status: ${wpErrorStatus}): ${wpErrorMessage}`);
|
|
380
|
+
console.error(` Full WordPress API error:`, wpError);
|
|
381
|
+
// Re-throw to ensure error is propagated
|
|
382
|
+
throw new Error(`Could not fetch menu ID ${menuId} via any method: ${wpErrorMessage}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
console.error(` ❌ Navigation post fetch failed with non-404 error, re-throwing...`);
|
|
387
|
+
throw navError; // Re-throw navigation error
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.error(` ❌ Menu term fetch failed with non-404 error (status: ${errorStatus}), re-throwing...`);
|
|
393
|
+
throw error; // Re-throw original error
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async getFooter(slug = 'footer', area = 'footer') {
|
|
398
|
+
return this.makeRequest(`/wp-custom/v1/footer`, { slug, area });
|
|
399
|
+
}
|
|
400
|
+
async getSettings() {
|
|
401
|
+
return this.makeRequest('/wp-custom/v1/settings');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let document;
|
|
406
|
+
let offset;
|
|
407
|
+
let output;
|
|
408
|
+
let stack;
|
|
409
|
+
const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g;
|
|
410
|
+
function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) {
|
|
411
|
+
return {
|
|
412
|
+
blockName,
|
|
413
|
+
attrs,
|
|
414
|
+
innerBlocks,
|
|
415
|
+
innerHTML,
|
|
416
|
+
innerContent
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function Freeform(innerHTML) {
|
|
420
|
+
return Block(null, {}, [], innerHTML, [innerHTML]);
|
|
421
|
+
}
|
|
422
|
+
function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) {
|
|
423
|
+
return {
|
|
424
|
+
block,
|
|
425
|
+
tokenStart,
|
|
426
|
+
tokenLength,
|
|
427
|
+
prevOffset: prevOffset || tokenStart + tokenLength,
|
|
428
|
+
leadingHtmlStart
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const parse = (doc) => {
|
|
432
|
+
document = doc;
|
|
433
|
+
offset = 0;
|
|
434
|
+
output = [];
|
|
435
|
+
stack = [];
|
|
436
|
+
tokenizer.lastIndex = 0;
|
|
437
|
+
do {
|
|
438
|
+
} while (proceed());
|
|
439
|
+
return output;
|
|
440
|
+
};
|
|
441
|
+
function proceed() {
|
|
442
|
+
const stackDepth = stack.length;
|
|
443
|
+
const next = nextToken();
|
|
444
|
+
const [tokenType, blockName, attrs, startOffset, tokenLength] = next;
|
|
445
|
+
const leadingHtmlStart = startOffset > offset ? offset : null;
|
|
446
|
+
switch (tokenType) {
|
|
447
|
+
case "no-more-tokens":
|
|
448
|
+
if (0 === stackDepth) {
|
|
449
|
+
addFreeform();
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (1 === stackDepth) {
|
|
453
|
+
addBlockFromStack();
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
while (0 < stack.length) {
|
|
457
|
+
addBlockFromStack();
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
case "void-block":
|
|
461
|
+
if (0 === stackDepth) {
|
|
462
|
+
if (null !== leadingHtmlStart) {
|
|
463
|
+
output.push(
|
|
464
|
+
Freeform(
|
|
465
|
+
document.substr(
|
|
466
|
+
leadingHtmlStart,
|
|
467
|
+
startOffset - leadingHtmlStart
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
output.push(Block(blockName, attrs, [], "", []));
|
|
473
|
+
offset = startOffset + tokenLength;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
addInnerBlock(
|
|
477
|
+
Block(blockName, attrs, [], "", []),
|
|
478
|
+
startOffset,
|
|
479
|
+
tokenLength
|
|
480
|
+
);
|
|
481
|
+
offset = startOffset + tokenLength;
|
|
482
|
+
return true;
|
|
483
|
+
case "block-opener":
|
|
484
|
+
stack.push(
|
|
485
|
+
Frame(
|
|
486
|
+
Block(blockName, attrs, [], "", []),
|
|
487
|
+
startOffset,
|
|
488
|
+
tokenLength,
|
|
489
|
+
startOffset + tokenLength,
|
|
490
|
+
leadingHtmlStart
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
offset = startOffset + tokenLength;
|
|
494
|
+
return true;
|
|
495
|
+
case "block-closer":
|
|
496
|
+
if (0 === stackDepth) {
|
|
497
|
+
addFreeform();
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
if (1 === stackDepth) {
|
|
501
|
+
addBlockFromStack(startOffset);
|
|
502
|
+
offset = startOffset + tokenLength;
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
const stackTop = stack.pop();
|
|
506
|
+
const html = document.substr(
|
|
507
|
+
stackTop.prevOffset,
|
|
508
|
+
startOffset - stackTop.prevOffset
|
|
509
|
+
);
|
|
510
|
+
stackTop.block.innerHTML += html;
|
|
511
|
+
stackTop.block.innerContent.push(html);
|
|
512
|
+
stackTop.prevOffset = startOffset + tokenLength;
|
|
513
|
+
addInnerBlock(
|
|
514
|
+
stackTop.block,
|
|
515
|
+
stackTop.tokenStart,
|
|
516
|
+
stackTop.tokenLength,
|
|
517
|
+
startOffset + tokenLength
|
|
518
|
+
);
|
|
519
|
+
offset = startOffset + tokenLength;
|
|
520
|
+
return true;
|
|
521
|
+
default:
|
|
522
|
+
addFreeform();
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function parseJSON(input) {
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(input);
|
|
529
|
+
} catch (e) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function nextToken() {
|
|
534
|
+
const matches = tokenizer.exec(document);
|
|
535
|
+
if (null === matches) {
|
|
536
|
+
return ["no-more-tokens", "", null, 0, 0];
|
|
537
|
+
}
|
|
538
|
+
const startedAt = matches.index;
|
|
539
|
+
const [
|
|
540
|
+
match,
|
|
541
|
+
closerMatch,
|
|
542
|
+
namespaceMatch,
|
|
543
|
+
nameMatch,
|
|
544
|
+
attrsMatch,
|
|
545
|
+
,
|
|
546
|
+
voidMatch
|
|
547
|
+
] = matches;
|
|
548
|
+
const length = match.length;
|
|
549
|
+
const isCloser = !!closerMatch;
|
|
550
|
+
const isVoid = !!voidMatch;
|
|
551
|
+
const namespace = namespaceMatch || "core/";
|
|
552
|
+
const name = namespace + nameMatch;
|
|
553
|
+
const hasAttrs = !!attrsMatch;
|
|
554
|
+
const attrs = hasAttrs ? parseJSON(attrsMatch) : {};
|
|
555
|
+
if (isVoid) {
|
|
556
|
+
return ["void-block", name, attrs, startedAt, length];
|
|
557
|
+
}
|
|
558
|
+
if (isCloser) {
|
|
559
|
+
return ["block-closer", name, null, startedAt, length];
|
|
560
|
+
}
|
|
561
|
+
return ["block-opener", name, attrs, startedAt, length];
|
|
562
|
+
}
|
|
563
|
+
function addFreeform(rawLength) {
|
|
564
|
+
const length = document.length - offset;
|
|
565
|
+
if (0 === length) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
output.push(Freeform(document.substr(offset, length)));
|
|
569
|
+
}
|
|
570
|
+
function addInnerBlock(block, tokenStart, tokenLength, lastOffset) {
|
|
571
|
+
const parent = stack[stack.length - 1];
|
|
572
|
+
parent.block.innerBlocks.push(block);
|
|
573
|
+
const html = document.substr(
|
|
574
|
+
parent.prevOffset,
|
|
575
|
+
tokenStart - parent.prevOffset
|
|
576
|
+
);
|
|
577
|
+
if (html) {
|
|
578
|
+
parent.block.innerHTML += html;
|
|
579
|
+
parent.block.innerContent.push(html);
|
|
580
|
+
}
|
|
581
|
+
parent.block.innerContent.push(null);
|
|
582
|
+
parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;
|
|
583
|
+
}
|
|
584
|
+
function addBlockFromStack(endOffset) {
|
|
585
|
+
const { block, leadingHtmlStart, prevOffset, tokenStart } = stack.pop();
|
|
586
|
+
const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset);
|
|
587
|
+
if (html) {
|
|
588
|
+
block.innerHTML += html;
|
|
589
|
+
block.innerContent.push(html);
|
|
590
|
+
}
|
|
591
|
+
if (null !== leadingHtmlStart) {
|
|
592
|
+
output.push(
|
|
593
|
+
Freeform(
|
|
594
|
+
document.substr(
|
|
595
|
+
leadingHtmlStart,
|
|
596
|
+
tokenStart - leadingHtmlStart
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
output.push(block);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* @license GPL-3.0-or-later
|
|
606
|
+
*
|
|
607
|
+
* This file is part of the MarVAlt Open SDK.
|
|
608
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
609
|
+
*
|
|
610
|
+
* This program is free software: you can redistribute it and/or modify
|
|
611
|
+
* it under the terms of the GNU General Public License as published by
|
|
612
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
613
|
+
* (at your option) any later version.
|
|
614
|
+
*
|
|
615
|
+
* This program is distributed in the hope that it will be useful,
|
|
616
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
617
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
618
|
+
* See the GNU General Public License for more details.
|
|
619
|
+
*/
|
|
620
|
+
// WordPress Static Data Generator
|
|
621
|
+
/**
|
|
622
|
+
* Extract menu items from a navigation block's innerHTML (rendered HTML)
|
|
623
|
+
* WordPress renders navigation blocks, so menu items are in innerHTML as HTML
|
|
624
|
+
* This function properly handles submenus by parsing the nested <ul> structure
|
|
625
|
+
*/
|
|
626
|
+
function extractMenuFromNavigationHTML(innerHTML) {
|
|
627
|
+
if (!innerHTML)
|
|
628
|
+
return null;
|
|
629
|
+
// Find the main navigation container <ul>
|
|
630
|
+
const mainNavMatch = innerHTML.match(/<ul[^>]*class="[^"]*wp-block-navigation__container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
|
|
631
|
+
if (!mainNavMatch) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
const mainNavContent = mainNavMatch[1];
|
|
635
|
+
const items = [];
|
|
636
|
+
let globalOrder = 0;
|
|
637
|
+
/**
|
|
638
|
+
* Parse a <li> element and extract menu item data
|
|
639
|
+
* Handles both regular links and submenus
|
|
640
|
+
*/
|
|
641
|
+
function parseMenuItem(liElement, parentId = 0, order = 0) {
|
|
642
|
+
// Extract the link
|
|
643
|
+
const linkMatch = liElement.match(/<a[^>]+class="[^"]*wp-block-navigation-item__content[^"]*"[^>]+href=["']([^"']+)["'][^>]*>[\s\S]*?<span[^>]*class="[^"]*wp-block-navigation-item__label[^"]*"[^>]*>([^<]+)<\/span>[\s\S]*?<\/a>/i);
|
|
644
|
+
if (!linkMatch) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const url = linkMatch[1].trim();
|
|
648
|
+
const title = linkMatch[2].trim();
|
|
649
|
+
if (!title) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
// Normalize URL
|
|
653
|
+
let finalUrl = url;
|
|
654
|
+
try {
|
|
655
|
+
const urlObj = new URL(url, 'http://dummy.com'); // Use dummy base for relative URLs
|
|
656
|
+
const isAbsoluteUrl = url.startsWith('http://') || url.startsWith('https://');
|
|
657
|
+
if (isAbsoluteUrl) {
|
|
658
|
+
const pathname = urlObj.pathname;
|
|
659
|
+
const isExternal = pathname === '' || pathname === '/' ||
|
|
660
|
+
!urlObj.hostname.includes('vibunedemos.com') &&
|
|
661
|
+
!urlObj.hostname.includes('localhost') &&
|
|
662
|
+
!urlObj.hostname.includes('127.0.0.1');
|
|
663
|
+
if (isExternal) {
|
|
664
|
+
finalUrl = url;
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
finalUrl = pathname + urlObj.search + urlObj.hash;
|
|
668
|
+
if (!finalUrl.startsWith('/')) {
|
|
669
|
+
finalUrl = `/${finalUrl}`;
|
|
670
|
+
}
|
|
671
|
+
if (finalUrl !== '/' && finalUrl.endsWith('/')) {
|
|
672
|
+
finalUrl = finalUrl.slice(0, -1);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
if (!finalUrl.startsWith('/')) {
|
|
678
|
+
finalUrl = `/${finalUrl}`;
|
|
679
|
+
}
|
|
680
|
+
if (finalUrl !== '/' && finalUrl.endsWith('/')) {
|
|
681
|
+
finalUrl = finalUrl.slice(0, -1);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://') && !finalUrl.startsWith('/')) {
|
|
687
|
+
finalUrl = `/${finalUrl}`;
|
|
688
|
+
}
|
|
689
|
+
if (finalUrl !== '/' && finalUrl.endsWith('/')) {
|
|
690
|
+
finalUrl = finalUrl.slice(0, -1);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const itemId = globalOrder + 1;
|
|
694
|
+
globalOrder++;
|
|
695
|
+
const menuItem = {
|
|
696
|
+
id: itemId,
|
|
697
|
+
title: title,
|
|
698
|
+
url: finalUrl,
|
|
699
|
+
type: 'custom',
|
|
700
|
+
object_id: 0,
|
|
701
|
+
object: 'custom',
|
|
702
|
+
parent: parentId,
|
|
703
|
+
menu_order: order,
|
|
704
|
+
};
|
|
705
|
+
// Check if this <li> contains a submenu (<ul class="wp-block-navigation__submenu-container">)
|
|
706
|
+
const submenuMatch = liElement.match(/<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
|
|
707
|
+
if (submenuMatch) {
|
|
708
|
+
const submenuContent = submenuMatch[1];
|
|
709
|
+
const children = [];
|
|
710
|
+
let childOrder = 0;
|
|
711
|
+
// Extract all <li> elements from the submenu
|
|
712
|
+
const submenuLiRegex = /<li[^>]*class="[^"]*wp-block-navigation-item[^"]*"[^>]*>([\s\S]*?)<\/li>/gi;
|
|
713
|
+
let submenuLiMatch;
|
|
714
|
+
while ((submenuLiMatch = submenuLiRegex.exec(submenuContent)) !== null) {
|
|
715
|
+
const childResult = parseMenuItem(submenuLiMatch[1], itemId, childOrder++);
|
|
716
|
+
if (childResult) {
|
|
717
|
+
children.push(childResult.item);
|
|
718
|
+
globalOrder = childResult.nextOrder;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (children.length > 0) {
|
|
722
|
+
menuItem.children = children;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return { item: menuItem, nextOrder: globalOrder };
|
|
726
|
+
}
|
|
727
|
+
// Extract all top-level <li> elements from the main navigation
|
|
728
|
+
// Find all <li> tags and extract complete items, filtering by position
|
|
729
|
+
const liRegex = /<li[^>]*>/gi;
|
|
730
|
+
const allLiMatches = [];
|
|
731
|
+
// First pass: find all <li> tags and their matching </li>
|
|
732
|
+
let match;
|
|
733
|
+
while ((match = liRegex.exec(mainNavContent)) !== null) {
|
|
734
|
+
const liStart = match.index;
|
|
735
|
+
let depth = 0;
|
|
736
|
+
let j = liStart;
|
|
737
|
+
while (j < mainNavContent.length) {
|
|
738
|
+
if (mainNavContent.substr(j, 3) === '<li' && /[>\s\n\t]/.test(mainNavContent[j + 3] || '')) {
|
|
739
|
+
depth++;
|
|
740
|
+
}
|
|
741
|
+
else if (mainNavContent.substr(j, 5) === '</li>') {
|
|
742
|
+
depth--;
|
|
743
|
+
if (depth === 0) {
|
|
744
|
+
allLiMatches.push({ start: liStart, end: j + 5 });
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
j++;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Second pass: filter to only top-level items (not inside submenu containers)
|
|
752
|
+
// Build a map of submenu container ranges
|
|
753
|
+
const submenuRanges = [];
|
|
754
|
+
const submenuContainerRegex = /<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>/gi;
|
|
755
|
+
let submenuMatch;
|
|
756
|
+
// Find all submenu containers and their matching </ul> tags
|
|
757
|
+
while ((submenuMatch = submenuContainerRegex.exec(mainNavContent)) !== null) {
|
|
758
|
+
const submenuStart = submenuMatch.index;
|
|
759
|
+
const submenuUlEnd = mainNavContent.indexOf('>', submenuStart);
|
|
760
|
+
if (submenuUlEnd === -1)
|
|
761
|
+
continue;
|
|
762
|
+
// Find matching </ul> by tracking depth
|
|
763
|
+
let ulDepth = 0;
|
|
764
|
+
let checkPos = submenuUlEnd + 1;
|
|
765
|
+
let submenuEnd = -1;
|
|
766
|
+
while (checkPos < mainNavContent.length) {
|
|
767
|
+
const ulOpen = mainNavContent.indexOf('<ul', checkPos);
|
|
768
|
+
const ulClose = mainNavContent.indexOf('</ul>', checkPos);
|
|
769
|
+
if (ulOpen !== -1 && (ulClose === -1 || ulOpen < ulClose)) {
|
|
770
|
+
ulDepth++;
|
|
771
|
+
checkPos = ulOpen + 3;
|
|
772
|
+
}
|
|
773
|
+
else if (ulClose !== -1) {
|
|
774
|
+
ulDepth--;
|
|
775
|
+
if (ulDepth === 0) {
|
|
776
|
+
submenuEnd = ulClose + 5;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
checkPos = ulClose + 5;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (submenuEnd !== -1) {
|
|
786
|
+
submenuRanges.push({ start: submenuStart, end: submenuEnd });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Filter <li> elements: exclude only those that are children inside submenu containers
|
|
790
|
+
// An <li> is a child (inside submenu) if: submenu's <ul> starts before <li> AND submenu's </ul> ends after <li> ends
|
|
791
|
+
// An <li> is a parent (contains submenu) if: <li> starts before submenu's <ul> (we want to include these)
|
|
792
|
+
for (const { start, end } of allLiMatches) {
|
|
793
|
+
// Check if this <li> is a child inside any submenu container
|
|
794
|
+
let isChildItem = false;
|
|
795
|
+
for (const range of submenuRanges) {
|
|
796
|
+
// If submenu <ul> starts before <li> AND submenu </ul> ends after <li> ends, then <li> is a child
|
|
797
|
+
if (range.start < start && range.end > end) {
|
|
798
|
+
isChildItem = true;
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Include all items except children (children will be extracted by parseMenuItem as part of their parent)
|
|
803
|
+
if (!isChildItem) {
|
|
804
|
+
const item = mainNavContent.substring(start, end);
|
|
805
|
+
if (item.includes('wp-block-navigation-item')) {
|
|
806
|
+
const result = parseMenuItem(item, 0, items.length);
|
|
807
|
+
if (result) {
|
|
808
|
+
items.push(result.item);
|
|
809
|
+
globalOrder = result.nextOrder;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (items.length === 0) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
id: 0, // No menu ID for inline menus
|
|
819
|
+
name: 'Navigation Menu',
|
|
820
|
+
slug: 'navigation-menu',
|
|
821
|
+
items: items,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Find a menu by name (checks both slug and name fields, case-insensitive)
|
|
826
|
+
* @param menus Array of menu objects
|
|
827
|
+
* @param menuNamePattern Name or slug pattern to match (case-insensitive)
|
|
828
|
+
* @returns Menu object if found, null otherwise
|
|
829
|
+
*/
|
|
830
|
+
async function findMenuByName(client, menuNamePattern) {
|
|
831
|
+
try {
|
|
832
|
+
console.log(`🔍 Fetching all menus to search for "${menuNamePattern}"...`);
|
|
833
|
+
const allMenus = await client.getMenus();
|
|
834
|
+
console.log(`📋 Found ${allMenus.length} menu(s):`, allMenus.map((m) => `${m.name} (slug: ${m.slug})`).join(', '));
|
|
835
|
+
const patternLower = menuNamePattern.toLowerCase();
|
|
836
|
+
// Find menu by slug or name (case-insensitive)
|
|
837
|
+
const foundMenu = allMenus.find((menu) => {
|
|
838
|
+
const slugMatch = menu.slug?.toLowerCase() === patternLower;
|
|
839
|
+
const nameMatch = menu.name?.toLowerCase() === patternLower;
|
|
840
|
+
// Also check if slug or name contains the pattern (for variations like "main-menu" vs "mainmenu")
|
|
841
|
+
const slugNormalized = menu.slug?.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
842
|
+
const nameNormalized = menu.name?.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
843
|
+
const patternNormalized = patternLower.replace(/[^a-z0-9]/g, '');
|
|
844
|
+
const slugContains = slugNormalized === patternNormalized;
|
|
845
|
+
const nameContains = nameNormalized === patternNormalized;
|
|
846
|
+
return slugMatch || nameMatch || slugContains || nameContains;
|
|
847
|
+
});
|
|
848
|
+
if (foundMenu) {
|
|
849
|
+
console.log(`✅ Found matching menu: ${foundMenu.name} (ID: ${foundMenu.id}, slug: ${foundMenu.slug})`);
|
|
850
|
+
// Fetch the full menu with items
|
|
851
|
+
const fullMenu = await client.getMenu(foundMenu.id);
|
|
852
|
+
return fullMenu;
|
|
853
|
+
}
|
|
854
|
+
console.log(`ℹ️ No menu found matching pattern "${menuNamePattern}"`);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.warn(`⚠️ Error finding menu by name "${menuNamePattern}":`, error);
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Extract menu items from a navigation block's innerBlocks
|
|
864
|
+
* Navigation blocks can have menu items stored as core/navigation-link blocks
|
|
865
|
+
*/
|
|
866
|
+
function extractMenuFromNavigationBlock(navBlock) {
|
|
867
|
+
if (!navBlock) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
// First, try to extract from innerBlocks if available
|
|
871
|
+
if (navBlock.innerBlocks && navBlock.innerBlocks.length > 0) {
|
|
872
|
+
const extractMenuItems = (blocks, parentId = 0, order = 0) => {
|
|
873
|
+
const items = [];
|
|
874
|
+
let currentOrder = order;
|
|
875
|
+
for (const block of blocks) {
|
|
876
|
+
if (block.name === 'core/navigation-link' || block.name === 'core/page-list' || block.name === 'core/navigation-submenu') {
|
|
877
|
+
const attrs = block.attributes || {};
|
|
878
|
+
const label = attrs.label || attrs.text || '';
|
|
879
|
+
const url = attrs.url || attrs.href || '';
|
|
880
|
+
const kind = attrs.kind || attrs.type || 'custom';
|
|
881
|
+
const id = attrs.id || attrs.objectId || currentOrder;
|
|
882
|
+
const menuItem = {
|
|
883
|
+
id: id,
|
|
884
|
+
title: label,
|
|
885
|
+
url: url,
|
|
886
|
+
type: kind,
|
|
887
|
+
object_id: attrs.objectId || 0,
|
|
888
|
+
object: attrs.type || 'custom',
|
|
889
|
+
parent: parentId,
|
|
890
|
+
menu_order: currentOrder++,
|
|
891
|
+
};
|
|
892
|
+
// Check for submenu items (children)
|
|
893
|
+
if (block.innerBlocks && block.innerBlocks.length > 0) {
|
|
894
|
+
const children = extractMenuItems(block.innerBlocks, id, 0);
|
|
895
|
+
if (children.length > 0) {
|
|
896
|
+
menuItem.children = children;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
items.push(menuItem);
|
|
900
|
+
}
|
|
901
|
+
else if (block.innerBlocks && block.innerBlocks.length > 0) {
|
|
902
|
+
// Recursively search in nested blocks
|
|
903
|
+
const nestedItems = extractMenuItems(block.innerBlocks, parentId, currentOrder);
|
|
904
|
+
items.push(...nestedItems);
|
|
905
|
+
currentOrder += nestedItems.length;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return items;
|
|
909
|
+
};
|
|
910
|
+
const items = extractMenuItems(navBlock.innerBlocks);
|
|
911
|
+
if (items.length > 0) {
|
|
912
|
+
return {
|
|
913
|
+
id: 0, // No menu ID for inline menus
|
|
914
|
+
name: 'Navigation Menu',
|
|
915
|
+
slug: 'navigation-menu',
|
|
916
|
+
items: items,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// Fallback: Try to extract from innerHTML if innerBlocks are empty
|
|
921
|
+
if (navBlock.innerHTML) {
|
|
922
|
+
return extractMenuFromNavigationHTML(navBlock.innerHTML);
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
class WordPressGenerator {
|
|
927
|
+
constructor(config) {
|
|
928
|
+
this.config = config;
|
|
929
|
+
}
|
|
930
|
+
async generateStaticData() {
|
|
931
|
+
const client = new WordPressClient(this.config);
|
|
932
|
+
console.log('🚀 Starting WordPress static data generation...');
|
|
933
|
+
const frontendId = this.config.frontendId ?? 'default-frontend';
|
|
934
|
+
const frontendName = this.config.frontendName || 'Default Frontend';
|
|
935
|
+
const postTypes = this.config.postTypes || ['posts', 'pages'];
|
|
936
|
+
// Use 'view' context - wp-custom-api-endpoints plugin adds blocks automatically.
|
|
937
|
+
// Frontends: when app provides frontendId, request Default + app frontend (OR); otherwise no filter (single-frontend backend).
|
|
938
|
+
// Use a custom slug-based param to avoid colliding with WordPress core taxonomy term ID filters.
|
|
939
|
+
const fetchParams = {
|
|
940
|
+
per_page: this.config.maxItems || 100,
|
|
941
|
+
_embed: true,
|
|
942
|
+
context: 'view', // Plugin adds blocks in 'view' context
|
|
943
|
+
};
|
|
944
|
+
if (this.config.frontendId && this.config.frontendId.trim() !== '') {
|
|
945
|
+
fetchParams.frontend_slugs = 'default,' + this.config.frontendId.trim();
|
|
946
|
+
}
|
|
947
|
+
console.log(`📋 Fetching post types: ${postTypes.join(', ')}`);
|
|
948
|
+
console.log(`⚙️ Settings: maxItems=${fetchParams.per_page}, includeEmbedded=${fetchParams._embed}`);
|
|
949
|
+
// Fetch WordPress Settings (Reading Settings + Site Branding + Theme Styles)
|
|
950
|
+
let frontPage;
|
|
951
|
+
let settingsPageId;
|
|
952
|
+
let siteSettings;
|
|
953
|
+
let themeStyles;
|
|
954
|
+
let header;
|
|
955
|
+
let footer;
|
|
956
|
+
let menuData;
|
|
957
|
+
let menus = {}; // Declare menus outside try block so it's accessible later
|
|
958
|
+
try {
|
|
959
|
+
console.log('🔍 Fetching WordPress Settings...');
|
|
960
|
+
const settings = await client.getSettings();
|
|
961
|
+
if (settings.front_page) {
|
|
962
|
+
// WordPress has a page set as front page in Reading Settings
|
|
963
|
+
frontPage = settings.front_page;
|
|
964
|
+
console.log(`✅ Front page determined from Reading Settings: ${frontPage.slug} (ID: ${frontPage.id})`);
|
|
965
|
+
}
|
|
966
|
+
else if (settings.show_on_front === 'page' && settings.page_on_front > 0) {
|
|
967
|
+
// Page is set but not returned in front_page (might be in pages list)
|
|
968
|
+
settingsPageId = settings.page_on_front;
|
|
969
|
+
console.log(`📋 Front page ID ${settingsPageId} set in Reading Settings, will check fetched pages`);
|
|
970
|
+
}
|
|
971
|
+
// Extract site branding settings
|
|
972
|
+
if (settings.logo || settings.site_icon || settings.site_name || settings.site_description) {
|
|
973
|
+
siteSettings = {
|
|
974
|
+
...(settings.logo && { logo: settings.logo }),
|
|
975
|
+
...(settings.site_icon && { site_icon: settings.site_icon }),
|
|
976
|
+
...(settings.site_name && { site_name: settings.site_name }),
|
|
977
|
+
...(settings.site_description && { site_description: settings.site_description }),
|
|
978
|
+
};
|
|
979
|
+
if (siteSettings.logo) {
|
|
980
|
+
console.log(`✅ Site logo found: ${siteSettings.logo.cloudflare_url || siteSettings.logo.url}`);
|
|
981
|
+
}
|
|
982
|
+
if (siteSettings.site_icon) {
|
|
983
|
+
console.log(`✅ Site icon found: ${siteSettings.site_icon.cloudflare_url || siteSettings.site_icon.url}`);
|
|
984
|
+
}
|
|
985
|
+
if (siteSettings.site_name) {
|
|
986
|
+
console.log(`✅ Site name: ${siteSettings.site_name}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// Extract theme styles
|
|
990
|
+
if (settings.theme_styles) {
|
|
991
|
+
themeStyles = settings.theme_styles;
|
|
992
|
+
console.log(`✅ Theme styles found:`, {
|
|
993
|
+
hasTypography: !!themeStyles.typography,
|
|
994
|
+
hasColors: !!themeStyles.colors,
|
|
995
|
+
hasLayout: !!themeStyles.layout,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
console.log('⚠️ No theme_styles found in settings response');
|
|
1000
|
+
}
|
|
1001
|
+
// Fetch header template part and extract menu
|
|
1002
|
+
console.log('🔍 Fetching Header template part...');
|
|
1003
|
+
try {
|
|
1004
|
+
header = await client.getHeader('header', 'header');
|
|
1005
|
+
if (header && header.blocks && header.blocks.length > 0) {
|
|
1006
|
+
console.log(`✅ Header found: ${header.blocks_count} blocks`);
|
|
1007
|
+
// Find navigation block and extract menu ID
|
|
1008
|
+
const findNavigationBlock = (blocks) => {
|
|
1009
|
+
for (const block of blocks) {
|
|
1010
|
+
if (block.name === 'core/navigation') {
|
|
1011
|
+
return block;
|
|
1012
|
+
}
|
|
1013
|
+
if (block.innerBlocks && block.innerBlocks.length > 0) {
|
|
1014
|
+
const found = findNavigationBlock(block.innerBlocks);
|
|
1015
|
+
if (found)
|
|
1016
|
+
return found;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return null;
|
|
1020
|
+
};
|
|
1021
|
+
const navBlock = findNavigationBlock(header.blocks);
|
|
1022
|
+
if (navBlock && navBlock.attributes) {
|
|
1023
|
+
// Debug: Log all navigation block attributes
|
|
1024
|
+
console.log('🔍 Navigation block attributes:', JSON.stringify(navBlock.attributes, null, 2));
|
|
1025
|
+
console.log('🔍 Navigation block innerBlocks:', navBlock.innerBlocks ? `${navBlock.innerBlocks.length} blocks` : 'none');
|
|
1026
|
+
// WordPress stores menu ID in 'ref' attribute (term_id of the menu)
|
|
1027
|
+
// Also check for other possible attribute names
|
|
1028
|
+
const menuId = navBlock.attributes['ref'] ||
|
|
1029
|
+
navBlock.attributes['menuId'] ||
|
|
1030
|
+
navBlock.attributes['menu'] ||
|
|
1031
|
+
navBlock.attributes['navigationMenuId'] ||
|
|
1032
|
+
navBlock.attributes['menuRef'];
|
|
1033
|
+
if (menuId) {
|
|
1034
|
+
try {
|
|
1035
|
+
console.log(`🔍 Found navigation block with menu ID: ${menuId}, fetching menu data...`);
|
|
1036
|
+
menuData = await client.getMenu(parseInt(menuId));
|
|
1037
|
+
console.log(`✅ Menu data fetched: ${menuData.items?.length || 0} items`);
|
|
1038
|
+
console.log('📋 Menu data structure:', JSON.stringify({
|
|
1039
|
+
id: menuData.id,
|
|
1040
|
+
name: menuData.name,
|
|
1041
|
+
itemsCount: menuData.items?.length || 0,
|
|
1042
|
+
firstItem: menuData.items?.[0] ? {
|
|
1043
|
+
id: menuData.items[0].id,
|
|
1044
|
+
title: menuData.items[0].title,
|
|
1045
|
+
url: menuData.items[0].url
|
|
1046
|
+
} : null
|
|
1047
|
+
}, null, 2));
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
console.warn(`⚠️ Could not fetch menu data for menu ID ${menuId}:`, error);
|
|
1051
|
+
// Fallback: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
|
|
1052
|
+
console.log('🔍 Menu fetch failed, attempting to find menu by name "mainmenu"...');
|
|
1053
|
+
try {
|
|
1054
|
+
const namedMenu = await findMenuByName(client, 'mainmenu');
|
|
1055
|
+
if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
|
|
1056
|
+
menuData = namedMenu;
|
|
1057
|
+
console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
console.log('ℹ️ No menu found with name "mainmenu"');
|
|
1061
|
+
// Try extracting from Navigation block HTML as last resort
|
|
1062
|
+
console.log('🔍 Attempting to extract navigation from Navigation block innerHTML...');
|
|
1063
|
+
try {
|
|
1064
|
+
const extractedMenu = extractMenuFromNavigationBlock(navBlock);
|
|
1065
|
+
if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
|
|
1066
|
+
menuData = extractedMenu;
|
|
1067
|
+
console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
catch (extractError) {
|
|
1071
|
+
console.warn('⚠️ Error extracting navigation from Navigation block:', extractError);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
catch (nameError) {
|
|
1076
|
+
console.warn('⚠️ Error finding menu by name:', nameError);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
console.log('ℹ️ Navigation block found but no menu ID attribute found');
|
|
1082
|
+
console.log('🔍 Available attributes:', Object.keys(navBlock.attributes));
|
|
1083
|
+
// Priority 2: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
|
|
1084
|
+
console.log('🔍 Attempting to find menu by name "mainmenu"...');
|
|
1085
|
+
try {
|
|
1086
|
+
const namedMenu = await findMenuByName(client, 'mainmenu');
|
|
1087
|
+
if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
|
|
1088
|
+
menuData = namedMenu;
|
|
1089
|
+
console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
console.log('ℹ️ No menu found with name "mainmenu"');
|
|
1093
|
+
// In block themes, navigation items may be stored directly in the Navigation block's innerBlocks
|
|
1094
|
+
// Try to extract from innerBlocks (this is how block themes work)
|
|
1095
|
+
console.log('🔍 Attempting to extract navigation from Navigation block innerBlocks...');
|
|
1096
|
+
try {
|
|
1097
|
+
const extractedMenu = extractMenuFromNavigationBlock(navBlock);
|
|
1098
|
+
if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
|
|
1099
|
+
menuData = extractedMenu;
|
|
1100
|
+
console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
|
|
1101
|
+
console.log('📋 Navigation items:', extractedMenu.items.map((item) => `${item.title} (${item.url})`).join(', '));
|
|
1102
|
+
console.log('💡 TIP: For better control, create a WordPress menu (Appearance > Menus) named "mainmenu" and assign it to the Navigation block.');
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
console.log('⚠️ WARNING: No navigation found in Navigation block. The frontend will fall back to page hierarchy.');
|
|
1106
|
+
console.log('💡 TIP: Add navigation items to the Navigation block in the Site Editor, or create a menu named "mainmenu" in WordPress (Appearance > Menus).');
|
|
1107
|
+
menuData = null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
console.warn('⚠️ Error extracting navigation from Navigation block:', error);
|
|
1112
|
+
console.log('⚠️ WARNING: The frontend will fall back to page hierarchy.');
|
|
1113
|
+
menuData = null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
console.warn('⚠️ Error finding menu by name:', error);
|
|
1119
|
+
console.log('ℹ️ Skipping HTML extraction - frontend will use page hierarchy fallback');
|
|
1120
|
+
menuData = null; // Don't use extracted menu - let frontend handle fallback
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
console.log('ℹ️ No navigation block found in header');
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
console.log('ℹ️ No header template part found');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
console.warn('⚠️ Could not fetch header template part:', error);
|
|
1134
|
+
}
|
|
1135
|
+
// Fetch footer template part
|
|
1136
|
+
console.log('🔍 Fetching Footer template part...');
|
|
1137
|
+
try {
|
|
1138
|
+
footer = await client.getFooter('footer', 'footer');
|
|
1139
|
+
if (footer && footer.blocks && footer.blocks.length > 0) {
|
|
1140
|
+
console.log(`✅ Footer found: ${footer.blocks_count} blocks`);
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
console.log('⚠️ Footer template part is empty or not found');
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
catch (error) {
|
|
1147
|
+
console.log('⚠️ Failed to fetch footer:', error instanceof Error ? error.message : 'Unknown error');
|
|
1148
|
+
}
|
|
1149
|
+
// Extract all menu IDs from footer blocks and fetch them for static data
|
|
1150
|
+
const allMenuIds = new Set();
|
|
1151
|
+
if (menuData && menuData.id) {
|
|
1152
|
+
allMenuIds.add(menuData.id); // Add header menu
|
|
1153
|
+
console.log(`📋 Added header menu ID ${menuData.id} (${menuData.name}) to menu list`);
|
|
1154
|
+
}
|
|
1155
|
+
if (footer && footer.blocks) {
|
|
1156
|
+
let navigationBlockCount = 0;
|
|
1157
|
+
const findNavigationBlocks = (blocks, depth = 0) => {
|
|
1158
|
+
for (const block of blocks) {
|
|
1159
|
+
if (block.name === 'core/navigation') {
|
|
1160
|
+
navigationBlockCount++;
|
|
1161
|
+
console.log(`🔍 Found navigation block at depth ${depth}:`, {
|
|
1162
|
+
blockName: block.name,
|
|
1163
|
+
hasAttributes: !!block.attributes,
|
|
1164
|
+
attributes: block.attributes ? Object.keys(block.attributes) : [],
|
|
1165
|
+
});
|
|
1166
|
+
if (block.attributes) {
|
|
1167
|
+
const menuId = block.attributes['ref'] ||
|
|
1168
|
+
block.attributes['menuId'] ||
|
|
1169
|
+
block.attributes['menu'] ||
|
|
1170
|
+
block.attributes['navigationMenuId'] ||
|
|
1171
|
+
block.attributes['menuRef'];
|
|
1172
|
+
if (menuId) {
|
|
1173
|
+
const menuIdNum = parseInt(String(menuId));
|
|
1174
|
+
allMenuIds.add(menuIdNum);
|
|
1175
|
+
console.log(` ✅ Extracted menu ID ${menuIdNum} from attribute:`, {
|
|
1176
|
+
ref: block.attributes['ref'],
|
|
1177
|
+
menuId: block.attributes['menuId'],
|
|
1178
|
+
menu: block.attributes['menu'],
|
|
1179
|
+
navigationMenuId: block.attributes['navigationMenuId'],
|
|
1180
|
+
menuRef: block.attributes['menuRef'],
|
|
1181
|
+
extracted: menuIdNum,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
console.warn(` ⚠️ Navigation block found but no menu ID attribute detected. Available attributes:`, Object.keys(block.attributes));
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
console.warn(` ⚠️ Navigation block found but has no attributes`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (block.innerBlocks && block.innerBlocks.length > 0) {
|
|
1193
|
+
findNavigationBlocks(block.innerBlocks, depth + 1);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
findNavigationBlocks(footer.blocks);
|
|
1198
|
+
console.log(`📊 Navigation block extraction summary:`, {
|
|
1199
|
+
navigationBlocksFound: navigationBlockCount,
|
|
1200
|
+
uniqueMenuIds: allMenuIds.size,
|
|
1201
|
+
menuIds: Array.from(allMenuIds),
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
console.log(`ℹ️ No footer blocks found or footer is empty`);
|
|
1206
|
+
}
|
|
1207
|
+
// Fetch all menus and store them in a menus object keyed by ID
|
|
1208
|
+
menus = {}; // Reset menus object (already declared above)
|
|
1209
|
+
if (menuData && menuData.id) {
|
|
1210
|
+
menus[menuData.id] = menuData;
|
|
1211
|
+
console.log(`✅ Added header menu to menus object: ID ${menuData.id} (${menuData.name})`);
|
|
1212
|
+
}
|
|
1213
|
+
let fetchAttempts = 0;
|
|
1214
|
+
let fetchSuccesses = 0;
|
|
1215
|
+
let fetchFailures = 0;
|
|
1216
|
+
for (const menuId of allMenuIds) {
|
|
1217
|
+
if (!menus[menuId]) {
|
|
1218
|
+
fetchAttempts++;
|
|
1219
|
+
console.log(`🔍 Attempting to fetch menu ID ${menuId}... (attempt ${fetchAttempts}/${allMenuIds.size})`);
|
|
1220
|
+
try {
|
|
1221
|
+
const fetchedMenu = await client.getMenu(menuId);
|
|
1222
|
+
// Validate menu structure
|
|
1223
|
+
if (!fetchedMenu) {
|
|
1224
|
+
console.error(`❌ Menu ID ${menuId}: Fetched menu is null or undefined`);
|
|
1225
|
+
fetchFailures++;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (!fetchedMenu.id) {
|
|
1229
|
+
console.error(`❌ Menu ID ${menuId}: Fetched menu missing 'id' property:`, fetchedMenu);
|
|
1230
|
+
fetchFailures++;
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (!fetchedMenu.name) {
|
|
1234
|
+
console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'name' property:`, fetchedMenu);
|
|
1235
|
+
}
|
|
1236
|
+
if (!fetchedMenu.items) {
|
|
1237
|
+
console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'items' property:`, fetchedMenu);
|
|
1238
|
+
}
|
|
1239
|
+
else if (!Array.isArray(fetchedMenu.items)) {
|
|
1240
|
+
console.error(`❌ Menu ID ${menuId}: Fetched menu 'items' is not an array:`, typeof fetchedMenu.items);
|
|
1241
|
+
fetchFailures++;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
// Store menu even if items array is empty (for debugging)
|
|
1245
|
+
menus[menuId] = fetchedMenu;
|
|
1246
|
+
if (fetchedMenu.items && fetchedMenu.items.length > 0) {
|
|
1247
|
+
fetchSuccesses++;
|
|
1248
|
+
console.log(`✅ Fetched menu ID ${menuId}: ${fetchedMenu.name} (${fetchedMenu.items.length} items)`);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
fetchSuccesses++; // Still count as success, just empty
|
|
1252
|
+
console.warn(`⚠️ Menu ID ${menuId}: ${fetchedMenu.name} fetched but has no items`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
fetchFailures++;
|
|
1257
|
+
const errorDetails = {
|
|
1258
|
+
message: error.message || 'Unknown error',
|
|
1259
|
+
status: error.status || 'unknown',
|
|
1260
|
+
};
|
|
1261
|
+
// Try to extract more error details
|
|
1262
|
+
if (error.response) {
|
|
1263
|
+
errorDetails.responseStatus = error.response.status;
|
|
1264
|
+
errorDetails.responseStatusText = error.response.statusText;
|
|
1265
|
+
}
|
|
1266
|
+
if (error.body) {
|
|
1267
|
+
try {
|
|
1268
|
+
errorDetails.body = typeof error.body === 'string' ? error.body : JSON.stringify(error.body);
|
|
1269
|
+
}
|
|
1270
|
+
catch (e) {
|
|
1271
|
+
errorDetails.body = 'Could not stringify error body';
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
console.error(`❌ Could not fetch menu ID ${menuId}:`, errorDetails);
|
|
1275
|
+
console.error(` Full error object:`, error);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
console.log(`ℹ️ Menu ID ${menuId} already in menus object, skipping fetch`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// Log menu fetching summary
|
|
1283
|
+
console.log(`📊 Menu fetching summary:`, {
|
|
1284
|
+
totalMenuIds: allMenuIds.size,
|
|
1285
|
+
fetchAttempts,
|
|
1286
|
+
fetchSuccesses,
|
|
1287
|
+
fetchFailures,
|
|
1288
|
+
menusInObject: Object.keys(menus).length,
|
|
1289
|
+
menuIdsInObject: Object.keys(menus).map(id => parseInt(id)),
|
|
1290
|
+
menuNames: Object.values(menus).map((m) => m.name || 'unnamed').join(', '),
|
|
1291
|
+
});
|
|
1292
|
+
// Debug: Dump all WordPress theme data in development
|
|
1293
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1294
|
+
const debugData = {
|
|
1295
|
+
timestamp: new Date().toISOString(),
|
|
1296
|
+
settings_response: settings,
|
|
1297
|
+
theme_styles_extracted: themeStyles,
|
|
1298
|
+
theme_styles_typography: themeStyles?.typography,
|
|
1299
|
+
theme_styles_colors: themeStyles?.colors,
|
|
1300
|
+
theme_styles_layout: themeStyles?.layout,
|
|
1301
|
+
theme_styles_palette: themeStyles?.theme_palette,
|
|
1302
|
+
};
|
|
1303
|
+
// Use same directory as output file (public/)
|
|
1304
|
+
const outputFullPath = path.join(process.cwd(), this.config.outputPath);
|
|
1305
|
+
const outputDir = path.dirname(outputFullPath);
|
|
1306
|
+
const debugPath = path.join(outputDir, 'wp-theme-data.json');
|
|
1307
|
+
try {
|
|
1308
|
+
fs.writeFileSync(debugPath, JSON.stringify(debugData, null, 2), 'utf-8');
|
|
1309
|
+
console.log(`📊 Debug: WordPress theme data dumped to ${debugPath}`);
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
console.warn('⚠️ Could not write debug file:', error);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
console.warn('⚠️ Could not fetch WordPress settings, will use fallback logic:', error);
|
|
1318
|
+
}
|
|
1319
|
+
// Fetch standard post types
|
|
1320
|
+
const data = await client.getAllData(fetchParams);
|
|
1321
|
+
// Determine front page if not already set from settings
|
|
1322
|
+
if (!frontPage) {
|
|
1323
|
+
if (settingsPageId) {
|
|
1324
|
+
// Try to find the page by ID from Reading Settings
|
|
1325
|
+
const settingsPage = (data.pages || []).find((p) => p.id === settingsPageId);
|
|
1326
|
+
if (settingsPage) {
|
|
1327
|
+
const title = typeof settingsPage.title === 'string'
|
|
1328
|
+
? settingsPage.title
|
|
1329
|
+
: (settingsPage.title?.rendered || 'Frontpage');
|
|
1330
|
+
frontPage = {
|
|
1331
|
+
id: settingsPage.id,
|
|
1332
|
+
slug: settingsPage.slug,
|
|
1333
|
+
title,
|
|
1334
|
+
};
|
|
1335
|
+
if (frontPage) {
|
|
1336
|
+
console.log(`✅ Front page determined from Reading Settings (ID match): ${frontPage.slug} (ID: ${frontPage.id})`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// Fallback: look for page with slug "frontpage"
|
|
1341
|
+
if (!frontPage) {
|
|
1342
|
+
const frontpagePage = (data.pages || []).find((p) => p.slug === 'frontpage');
|
|
1343
|
+
if (frontpagePage) {
|
|
1344
|
+
const title = typeof frontpagePage.title === 'string'
|
|
1345
|
+
? frontpagePage.title
|
|
1346
|
+
: (frontpagePage.title?.rendered || 'Frontpage');
|
|
1347
|
+
frontPage = {
|
|
1348
|
+
id: frontpagePage.id,
|
|
1349
|
+
slug: frontpagePage.slug,
|
|
1350
|
+
title,
|
|
1351
|
+
};
|
|
1352
|
+
if (frontPage) {
|
|
1353
|
+
console.log(`✅ Front page determined from slug "frontpage": ${frontPage.slug} (ID: ${frontPage.id})`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
console.log('⚠️ No front page found (neither in Reading Settings nor slug "frontpage")');
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// Create the static data structure
|
|
1362
|
+
const staticData = {
|
|
1363
|
+
generated_at: new Date().toISOString(),
|
|
1364
|
+
frontend_id: frontendId,
|
|
1365
|
+
frontend_name: frontendName,
|
|
1366
|
+
...(frontPage && { front_page: frontPage }),
|
|
1367
|
+
...(siteSettings && { site_settings: siteSettings }),
|
|
1368
|
+
...(header && { header: header }),
|
|
1369
|
+
...(menuData && { navigation_menu: menuData }),
|
|
1370
|
+
menus: menus, // Always include menus object, even if empty, for debugging
|
|
1371
|
+
...(footer && { footer: footer }),
|
|
1372
|
+
...(themeStyles && { theme_styles: themeStyles }),
|
|
1373
|
+
config: {
|
|
1374
|
+
frontend_id: frontendId,
|
|
1375
|
+
frontend_name: frontendName,
|
|
1376
|
+
post_types: postTypes.reduce((acc, postType) => {
|
|
1377
|
+
acc[postType] = {
|
|
1378
|
+
max_items: this.config.maxItems || 100,
|
|
1379
|
+
include_embedded: this.config.includeEmbedded || true,
|
|
1380
|
+
orderby: 'date',
|
|
1381
|
+
order: 'desc',
|
|
1382
|
+
};
|
|
1383
|
+
return acc;
|
|
1384
|
+
}, {}),
|
|
1385
|
+
},
|
|
1386
|
+
// Map standard post types
|
|
1387
|
+
posts: data.posts,
|
|
1388
|
+
pages: await Promise.all((data.pages || []).map(async (p) => {
|
|
1389
|
+
// Always use custom blocks endpoint to ensure we get the latest blocks with complete Cloudflare URLs
|
|
1390
|
+
// The custom endpoint (/wp-custom/v1/pages/{id}/blocks) is more reliable and always has the latest data
|
|
1391
|
+
let blocks = [];
|
|
1392
|
+
try {
|
|
1393
|
+
console.log(`🌐 Fetching blocks from custom endpoint for page "${p.slug}" (ID: ${p.id})...`);
|
|
1394
|
+
const blocksResponse = await client.getPageBlocks(p.id);
|
|
1395
|
+
console.log(`📦 Blocks response for "${p.slug}":`, blocksResponse ? `has blocks=${Array.isArray(blocksResponse.blocks) ? blocksResponse.blocks.length : 'N/A'}` : 'null');
|
|
1396
|
+
if (blocksResponse && Array.isArray(blocksResponse.blocks) && blocksResponse.blocks.length > 0) {
|
|
1397
|
+
blocks = blocksResponse.blocks;
|
|
1398
|
+
console.log(`✅ Fetched ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from custom endpoint`);
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
// Fallback: try blocks from API response if custom endpoint is empty
|
|
1402
|
+
const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
|
|
1403
|
+
if (blocksFromResponse.length > 0) {
|
|
1404
|
+
blocks = blocksFromResponse;
|
|
1405
|
+
console.log(`⚠️ Custom endpoint returned no blocks, using ${blocks.length} blocks from API response for page "${p.slug}" (ID: ${p.id})`);
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
console.warn(`⚠️ Custom endpoint returned no blocks for page "${p.slug}" (ID: ${p.id})`);
|
|
1409
|
+
// Last resort: try parsing from content.raw (but this won't work in 'view' context)
|
|
1410
|
+
const raw = p?.content?.raw || '';
|
|
1411
|
+
if (raw) {
|
|
1412
|
+
try {
|
|
1413
|
+
blocks = parse(raw);
|
|
1414
|
+
if (blocks.length > 0) {
|
|
1415
|
+
console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
catch (parseError) {
|
|
1419
|
+
console.warn(`⚠️ Failed to parse blocks from content.raw for page "${p.slug}" (ID: ${p.id}):`, parseError);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
console.warn(`⚠️ Page "${p.slug}" (ID: ${p.id}) has no blocks and no content.raw. Plugin may not be active or working.`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
catch (e) {
|
|
1429
|
+
console.warn(`⚠️ Failed to fetch blocks from custom endpoint for page "${p.slug}" (ID: ${p.id}):`, e.message);
|
|
1430
|
+
// Fallback: try blocks from API response
|
|
1431
|
+
const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
|
|
1432
|
+
if (blocksFromResponse.length > 0) {
|
|
1433
|
+
blocks = blocksFromResponse;
|
|
1434
|
+
console.log(`⚠️ Using ${blocks.length} blocks from API response as fallback for page "${p.slug}" (ID: ${p.id})`);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
// Last resort: try parsing from content.raw
|
|
1438
|
+
try {
|
|
1439
|
+
const raw = p?.content?.raw || '';
|
|
1440
|
+
if (raw) {
|
|
1441
|
+
blocks = parse(raw);
|
|
1442
|
+
if (blocks.length > 0) {
|
|
1443
|
+
console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw (fallback)`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
catch (parseError) {
|
|
1448
|
+
// Silent fail - we've already logged the warning
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return { ...p, blocks };
|
|
1453
|
+
})),
|
|
1454
|
+
media: data.media,
|
|
1455
|
+
categories: data.categories,
|
|
1456
|
+
tags: data.tags,
|
|
1457
|
+
};
|
|
1458
|
+
// Fetch custom post types (anything beyond the standard posts, pages, media, categories, tags)
|
|
1459
|
+
const standardTypes = ['posts', 'pages', 'media', 'categories', 'tags'];
|
|
1460
|
+
const customPostTypes = postTypes.filter(pt => !standardTypes.includes(pt));
|
|
1461
|
+
if (customPostTypes.length > 0) {
|
|
1462
|
+
console.log(`📦 Fetching custom post types: ${customPostTypes.join(', ')}`);
|
|
1463
|
+
for (const postType of customPostTypes) {
|
|
1464
|
+
try {
|
|
1465
|
+
console.log(`\n🔍 Fetching custom post type: ${postType}`);
|
|
1466
|
+
const customData = await client.getCustomPostType(postType, fetchParams);
|
|
1467
|
+
staticData[postType] = customData;
|
|
1468
|
+
console.log(`✅ ${postType}: ${customData.length} items fetched`);
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
const msg = error?.message || String(error);
|
|
1472
|
+
const status = error?.response?.status ?? error?.status;
|
|
1473
|
+
console.warn(`⚠️ Failed to fetch custom post type '${postType}':`, msg);
|
|
1474
|
+
if (status)
|
|
1475
|
+
console.warn(` HTTP status: ${status}`);
|
|
1476
|
+
if (error?.response?.statusText)
|
|
1477
|
+
console.warn(` Status text: ${error.response.statusText}`);
|
|
1478
|
+
staticData[postType] = [];
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
// Enrich CPT items that have featured_media with Cloudflare URL from media list
|
|
1482
|
+
// (API may return WordPress URLs in featured_image_urls; media has cloudflare_image_standardized)
|
|
1483
|
+
const mediaById = new Map();
|
|
1484
|
+
(data.media || []).forEach((m) => {
|
|
1485
|
+
const url = m.cloudflare_image_standardized || m.cloudflare_url;
|
|
1486
|
+
if (url)
|
|
1487
|
+
mediaById.set(m.id, url);
|
|
1488
|
+
});
|
|
1489
|
+
if (mediaById.size > 0) {
|
|
1490
|
+
for (const postType of customPostTypes) {
|
|
1491
|
+
const items = staticData[postType];
|
|
1492
|
+
if (!Array.isArray(items))
|
|
1493
|
+
continue;
|
|
1494
|
+
let enriched = 0;
|
|
1495
|
+
items.forEach((item) => {
|
|
1496
|
+
const fid = item.featured_media ? Number(item.featured_media) : 0;
|
|
1497
|
+
if (fid && mediaById.has(fid)) {
|
|
1498
|
+
item.featured_media_cloudflare_url = mediaById.get(fid);
|
|
1499
|
+
enriched++;
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
if (enriched > 0) {
|
|
1503
|
+
console.log(`✅ Enriched ${enriched} ${postType} with featured_media_cloudflare_url`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// Write to file
|
|
1509
|
+
this.writeStaticData(staticData);
|
|
1510
|
+
console.log('\n✅ WordPress static data generation completed');
|
|
1511
|
+
console.log(`📊 Standard data: ${data.posts.length} posts, ${data.pages.length} pages, ${data.media.length} media`);
|
|
1512
|
+
if (customPostTypes.length > 0) {
|
|
1513
|
+
const customCounts = customPostTypes.map(pt => `${pt}: ${staticData[pt]?.length || 0}`).join(', ');
|
|
1514
|
+
console.log(`📊 Custom data: ${customCounts}`);
|
|
1515
|
+
}
|
|
1516
|
+
return staticData;
|
|
1517
|
+
}
|
|
1518
|
+
writeStaticData(data) {
|
|
1519
|
+
try {
|
|
1520
|
+
const outputPath = path.join(process.cwd(), this.config.outputPath);
|
|
1521
|
+
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
|
1522
|
+
console.log(`📁 Static data written to: ${outputPath}`);
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
console.error('❌ Error writing static data:', error);
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
async generatePostsOnly() {
|
|
1530
|
+
const client = new WordPressClient(this.config);
|
|
1531
|
+
return client.getPosts({ per_page: 100 });
|
|
1532
|
+
}
|
|
1533
|
+
async generatePagesOnly() {
|
|
1534
|
+
const client = new WordPressClient(this.config);
|
|
1535
|
+
return client.getPages({ per_page: 100 });
|
|
1536
|
+
}
|
|
1537
|
+
async generateMediaOnly() {
|
|
1538
|
+
const client = new WordPressClient(this.config);
|
|
1539
|
+
return client.getMedia({ per_page: 100 });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
// Convenience function for easy usage
|
|
1543
|
+
async function generateWordPressData$1(config) {
|
|
1544
|
+
const generator = new WordPressGenerator(config);
|
|
1545
|
+
return generator.generateStaticData();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* @license GPL-3.0-or-later
|
|
1550
|
+
*
|
|
1551
|
+
* This file is part of the MarVAlt Open SDK.
|
|
1552
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
1553
|
+
*
|
|
1554
|
+
* This program is free software: you can redistribute it and/or modify
|
|
1555
|
+
* it under the terms of the GNU General Public License as published by
|
|
1556
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
1557
|
+
* (at your option) any later version.
|
|
1558
|
+
*
|
|
1559
|
+
* This program is distributed in the hope that it will be useful,
|
|
1560
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
1561
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
1562
|
+
* See the GNU General Public License for more details.
|
|
1563
|
+
*/
|
|
1564
|
+
class GravityFormsClient {
|
|
1565
|
+
constructor(config) {
|
|
1566
|
+
this.config = config;
|
|
1567
|
+
// Determine mode: direct for generators (Node.js), proxy for browser
|
|
1568
|
+
const hasCredentials = !!(config.username && config.password && config.username.length > 0 && config.password.length > 0);
|
|
1569
|
+
this.useDirectMode = config.authMode === 'direct' && hasCredentials;
|
|
1570
|
+
// Debug logging for Node.js environments (generators)
|
|
1571
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
1572
|
+
console.log('🔍 GravityFormsClient Configuration:', {
|
|
1573
|
+
authMode: config.authMode,
|
|
1574
|
+
hasUsername: !!config.username,
|
|
1575
|
+
hasPassword: !!config.password,
|
|
1576
|
+
hasApiUrl: !!config.apiUrl,
|
|
1577
|
+
useDirectMode: this.useDirectMode,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
// Convention-based: Always use /api/gravity-forms-submit unless explicitly overridden
|
|
1581
|
+
this.proxyEndpoint = config.proxyEndpoint || '/api/gravity-forms-submit';
|
|
1582
|
+
}
|
|
1583
|
+
async makeRequest(endpoint, options = {}) {
|
|
1584
|
+
let url;
|
|
1585
|
+
const headers = {
|
|
1586
|
+
'Content-Type': 'application/json',
|
|
1587
|
+
...options.headers,
|
|
1588
|
+
};
|
|
1589
|
+
if (this.useDirectMode) {
|
|
1590
|
+
// Direct mode: Call WordPress API directly (for generators/Node.js)
|
|
1591
|
+
url = `${this.config.apiUrl}${endpoint}`;
|
|
1592
|
+
// Add Basic Auth
|
|
1593
|
+
if (this.config.username && this.config.password) {
|
|
1594
|
+
const credentials = btoa(`${this.config.username}:${this.config.password}`);
|
|
1595
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
1596
|
+
}
|
|
1597
|
+
// Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
|
|
1598
|
+
if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
|
|
1599
|
+
headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
|
|
1600
|
+
headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
|
|
1601
|
+
console.log('🔐 Added CF Access Service Token headers to Gravity Forms request');
|
|
1602
|
+
console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
|
|
1603
|
+
console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
|
|
1607
|
+
console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
|
|
1608
|
+
console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
// Proxy mode: Use Pages Function (for browser)
|
|
1613
|
+
const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
1614
|
+
url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
|
|
1615
|
+
}
|
|
1616
|
+
const response = await fetch(url, {
|
|
1617
|
+
...options,
|
|
1618
|
+
headers,
|
|
1619
|
+
signal: AbortSignal.timeout(this.config.timeout || 30000),
|
|
1620
|
+
});
|
|
1621
|
+
const responseText = await response.text();
|
|
1622
|
+
if (!response.ok) {
|
|
1623
|
+
// Try to parse error response for validation details
|
|
1624
|
+
let errorDetails = null;
|
|
1625
|
+
try {
|
|
1626
|
+
if (responseText) {
|
|
1627
|
+
errorDetails = JSON.parse(responseText);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
catch (e) {
|
|
1631
|
+
// Ignore parse errors, use generic error
|
|
1632
|
+
}
|
|
1633
|
+
// Provide helpful error message for 404 (plugin not installed/activated)
|
|
1634
|
+
if (response.status === 404) {
|
|
1635
|
+
const errorMsg = `Gravity Forms API request failed: ${response.status} ${response.statusText}\n` +
|
|
1636
|
+
`The endpoint ${url} was not found.\n` +
|
|
1637
|
+
`This usually means the 'gravity-forms-api-endpoint' plugin is not installed or activated on your WordPress site.\n` +
|
|
1638
|
+
`Please verify:\n` +
|
|
1639
|
+
` 1. The plugin is installed in /wp-content/plugins/gravity-forms-api-endpoint/\n` +
|
|
1640
|
+
` 2. The plugin is activated in WordPress admin\n` +
|
|
1641
|
+
` 3. The endpoint is accessible: ${this.config.apiUrl}/health`;
|
|
1642
|
+
throw new Error(errorMsg);
|
|
1643
|
+
}
|
|
1644
|
+
// For 400 errors, include validation details if available
|
|
1645
|
+
// WordPress REST API formats WP_Error as: { code, message, data: { status, validation_errors? } }
|
|
1646
|
+
if (response.status === 400 && errorDetails) {
|
|
1647
|
+
// Debug logging to see the actual error structure
|
|
1648
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
|
|
1649
|
+
console.log('🔍 GravityFormsClient 400 error details:', JSON.stringify(errorDetails, null, 2));
|
|
1650
|
+
}
|
|
1651
|
+
const error = new Error(errorDetails.message || `Gravity Forms API request failed: ${response.status} ${response.statusText}`);
|
|
1652
|
+
error.data = errorDetails;
|
|
1653
|
+
error.code = errorDetails.code;
|
|
1654
|
+
// Extract validation_errors from WordPress REST API format
|
|
1655
|
+
if (errorDetails.data?.validation_errors) {
|
|
1656
|
+
error.validation_errors = errorDetails.data.validation_errors;
|
|
1657
|
+
}
|
|
1658
|
+
throw error;
|
|
1659
|
+
}
|
|
1660
|
+
throw new Error(`Gravity Forms API request failed: ${response.status} ${response.statusText}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (!responseText) {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
try {
|
|
1666
|
+
return JSON.parse(responseText);
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
console.error('❌ Gravity Forms JSON Parse Error:', {
|
|
1670
|
+
error: error.message,
|
|
1671
|
+
responseText: responseText.substring(0, 500),
|
|
1672
|
+
});
|
|
1673
|
+
throw new Error(`Failed to parse JSON response: ${error.message}`);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async getForm(id) {
|
|
1677
|
+
// Always use custom gf-api/v1 endpoint (from custom plugin)
|
|
1678
|
+
const endpoint = `/forms/${id}`;
|
|
1679
|
+
return this.makeRequest(endpoint);
|
|
1680
|
+
}
|
|
1681
|
+
async getForms() {
|
|
1682
|
+
// Always use custom gf-api/v1 endpoint (from custom plugin)
|
|
1683
|
+
const endpoint = '/forms';
|
|
1684
|
+
return this.makeRequest(endpoint);
|
|
1685
|
+
}
|
|
1686
|
+
async getFormConfig(id) {
|
|
1687
|
+
// Always use custom gf-api/v1 endpoint (from custom plugin)
|
|
1688
|
+
const endpoint = `/forms/${id}/config`;
|
|
1689
|
+
return this.makeRequest(endpoint);
|
|
1690
|
+
}
|
|
1691
|
+
async submitForm(formId, submission) {
|
|
1692
|
+
// Always use custom gf-api/v1 submit endpoint (from custom plugin)
|
|
1693
|
+
const endpoint = `/forms/${formId}/submit`;
|
|
1694
|
+
// Debug logging
|
|
1695
|
+
console.log('🔍 GravityFormsClient.submitForm called:', {
|
|
1696
|
+
formId,
|
|
1697
|
+
field_values: submission.field_values,
|
|
1698
|
+
field_values_stringified: JSON.stringify(submission.field_values),
|
|
1699
|
+
has_headers: !!submission.headers,
|
|
1700
|
+
});
|
|
1701
|
+
// Merge custom headers (e.g., Turnstile token) with default headers
|
|
1702
|
+
const headers = {
|
|
1703
|
+
'Content-Type': 'application/json',
|
|
1704
|
+
...(submission.headers || {}),
|
|
1705
|
+
};
|
|
1706
|
+
return this.makeRequest(endpoint, {
|
|
1707
|
+
method: 'POST',
|
|
1708
|
+
headers,
|
|
1709
|
+
body: JSON.stringify(submission.field_values),
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
async getHealth() {
|
|
1713
|
+
const endpoint = '/health';
|
|
1714
|
+
return this.makeRequest(endpoint);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* @license GPL-3.0-or-later
|
|
1720
|
+
*
|
|
1721
|
+
* This file is part of the MarVAlt Open SDK.
|
|
1722
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
1723
|
+
*
|
|
1724
|
+
* This program is free software: you can redistribute it and/or modify
|
|
1725
|
+
* it under the terms of the GNU General Public License as published by
|
|
1726
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
1727
|
+
* (at your option) any later version.
|
|
1728
|
+
*
|
|
1729
|
+
* This program is distributed in the hope that it will be useful,
|
|
1730
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
1731
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
1732
|
+
* See the GNU General Public License for more details.
|
|
1733
|
+
*/
|
|
1734
|
+
// Gravity Forms Static Data Generator
|
|
1735
|
+
class GravityFormsGenerator {
|
|
1736
|
+
constructor(config) {
|
|
1737
|
+
this.config = config;
|
|
1738
|
+
}
|
|
1739
|
+
async generateStaticData() {
|
|
1740
|
+
const client = new GravityFormsClient(this.config);
|
|
1741
|
+
console.log('🚀 Starting Gravity Forms static data generation...');
|
|
1742
|
+
let forms = [];
|
|
1743
|
+
if (this.config.formIds && this.config.formIds.length > 0) {
|
|
1744
|
+
// Generate specific forms
|
|
1745
|
+
for (const formId of this.config.formIds) {
|
|
1746
|
+
try {
|
|
1747
|
+
const form = await client.getFormConfig(formId);
|
|
1748
|
+
if (this.config.includeInactive || form.is_active) {
|
|
1749
|
+
forms.push(form);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
console.warn(`⚠️ Could not fetch form ${formId}:`, error);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
// Generate all forms
|
|
1759
|
+
const formsList = await client.getForms();
|
|
1760
|
+
console.log('🔍 Gravity Forms Response:', {
|
|
1761
|
+
formsType: typeof formsList,
|
|
1762
|
+
isArray: Array.isArray(formsList),
|
|
1763
|
+
formsLength: formsList?.length,
|
|
1764
|
+
formsPreview: formsList ? JSON.stringify(formsList).substring(0, 200) : 'null',
|
|
1765
|
+
});
|
|
1766
|
+
let formsMetadata = [];
|
|
1767
|
+
if (Array.isArray(formsList)) {
|
|
1768
|
+
formsMetadata = formsList;
|
|
1769
|
+
}
|
|
1770
|
+
else if (formsList && typeof formsList === 'object' && Array.isArray(formsList.forms)) {
|
|
1771
|
+
formsMetadata = formsList.forms;
|
|
1772
|
+
}
|
|
1773
|
+
else if (formsList) {
|
|
1774
|
+
formsMetadata = [formsList];
|
|
1775
|
+
}
|
|
1776
|
+
if (!this.config.includeInactive) {
|
|
1777
|
+
formsMetadata = formsMetadata.filter(form => form.is_active === '1' || form.is_active === true);
|
|
1778
|
+
}
|
|
1779
|
+
// Fetch full form configuration for each form (schema lives under /config)
|
|
1780
|
+
console.log(`🔄 Fetching full data for ${formsMetadata.length} forms...`);
|
|
1781
|
+
forms = [];
|
|
1782
|
+
for (const formMetadata of formsMetadata) {
|
|
1783
|
+
try {
|
|
1784
|
+
const formId = Number(formMetadata.id);
|
|
1785
|
+
const fullForm = await client.getFormConfig(formId);
|
|
1786
|
+
forms.push(fullForm);
|
|
1787
|
+
console.log(`✅ Fetched form: ${fullForm.title} (${fullForm.fields?.length || 0} fields)`);
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
console.error(`❌ Failed to fetch form ${formMetadata.id} config:`, error);
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
const staticData = {
|
|
1796
|
+
generated_at: new Date().toISOString(),
|
|
1797
|
+
total_forms: forms.length,
|
|
1798
|
+
forms,
|
|
1799
|
+
};
|
|
1800
|
+
// Write to file
|
|
1801
|
+
this.writeStaticData(staticData);
|
|
1802
|
+
console.log('✅ Gravity Forms static data generation completed');
|
|
1803
|
+
console.log(`📊 Generated data: ${forms.length} forms`);
|
|
1804
|
+
return staticData;
|
|
1805
|
+
}
|
|
1806
|
+
writeStaticData(data) {
|
|
1807
|
+
try {
|
|
1808
|
+
const outputPath = path.join(process.cwd(), this.config.outputPath);
|
|
1809
|
+
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
|
1810
|
+
console.log(`📁 Static data written to: ${outputPath}`);
|
|
1811
|
+
}
|
|
1812
|
+
catch (error) {
|
|
1813
|
+
console.error('❌ Error writing static data:', error);
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async generateFormConfig(formId) {
|
|
1818
|
+
const client = new GravityFormsClient(this.config);
|
|
1819
|
+
return client.getFormConfig(formId);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
// Convenience function for easy usage
|
|
1823
|
+
async function generateGravityFormsData$1(config) {
|
|
1824
|
+
const generator = new GravityFormsGenerator(config);
|
|
1825
|
+
return generator.generateStaticData();
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
async function generateWordPressData(options) {
|
|
1829
|
+
const outputPath = options?.outputPath || './public/wordpress-data.json';
|
|
1830
|
+
const apiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
|
|
1831
|
+
const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME;
|
|
1832
|
+
const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD;
|
|
1833
|
+
const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
|
|
1834
|
+
const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
|
|
1835
|
+
const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
|
|
1836
|
+
if (!apiUrl) {
|
|
1837
|
+
console.error('❌ Missing WordPress API URL. Set WORDPRESS_API_URL or VITE_WORDPRESS_API_URL');
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
|
|
1841
|
+
const requestedAuth = process.env.VITE_AUTH_MODE;
|
|
1842
|
+
const hasProxy = !!cloudflareWorkerUrl;
|
|
1843
|
+
const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
|
|
1844
|
+
const config = {
|
|
1845
|
+
authMode,
|
|
1846
|
+
cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
|
|
1847
|
+
apiUrl,
|
|
1848
|
+
username,
|
|
1849
|
+
password,
|
|
1850
|
+
cfAccessClientId,
|
|
1851
|
+
cfAccessClientSecret,
|
|
1852
|
+
};
|
|
1853
|
+
const enabledPostTypes = process.env.VITE_ENABLED_POST_TYPES || process.env.ENABLED_POST_TYPES || 'posts,pages';
|
|
1854
|
+
const rawPostTypes = enabledPostTypes.split(',').map(pt => pt.trim()).filter(Boolean);
|
|
1855
|
+
const postTypeMapping = {
|
|
1856
|
+
'members': 'member',
|
|
1857
|
+
'testimonials': 'testimonial',
|
|
1858
|
+
'faqs': 'faq',
|
|
1859
|
+
'eventbrite_events': 'eventbrite_event',
|
|
1860
|
+
};
|
|
1861
|
+
const postTypes = rawPostTypes.map(pt => postTypeMapping[pt] || pt);
|
|
1862
|
+
const frontendId = process.env.VITE_FRONTEND_ID || process.env.FRONTEND_ID || undefined;
|
|
1863
|
+
const frontendName = process.env.VITE_FRONTEND_NAME || process.env.FRONTEND_NAME || 'Default Frontend';
|
|
1864
|
+
try {
|
|
1865
|
+
const result = await generateWordPressData$1({
|
|
1866
|
+
...config,
|
|
1867
|
+
outputPath,
|
|
1868
|
+
postTypes,
|
|
1869
|
+
...(frontendId && { frontendId }),
|
|
1870
|
+
frontendName,
|
|
1871
|
+
includeEmbedded: true,
|
|
1872
|
+
maxItems: parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '200', 10),
|
|
1873
|
+
});
|
|
1874
|
+
console.log(`📁 WordPress data written to: ${path.join(process.cwd(), outputPath)}`);
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
catch (e) {
|
|
1878
|
+
console.error('❌ WordPress generation failed:', e.message);
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
async function generateGravityFormsData(options) {
|
|
1884
|
+
const outputPath = options?.outputPath || './public/gravity-forms-data.json';
|
|
1885
|
+
const wpApiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
|
|
1886
|
+
const gfApiUrl = process.env.GRAVITY_FORMS_API_URL || process.env.VITE_GRAVITY_FORMS_API_URL;
|
|
1887
|
+
const username = process.env.GRAVITY_FORMS_USERNAME || process.env.VITE_GRAVITY_FORMS_USERNAME ||
|
|
1888
|
+
process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
|
|
1889
|
+
const password = process.env.GRAVITY_FORMS_PASSWORD || process.env.VITE_GRAVITY_FORMS_PASSWORD ||
|
|
1890
|
+
process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
|
|
1891
|
+
const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
|
|
1892
|
+
const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
|
|
1893
|
+
const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
|
|
1894
|
+
const apiUrl = gfApiUrl || (wpApiUrl ? `${wpApiUrl}/wp-json/gf-api/v1` : undefined);
|
|
1895
|
+
if (!apiUrl) {
|
|
1896
|
+
console.error('❌ Missing WordPress/Gravity Forms API URL.');
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
|
|
1900
|
+
const requestedAuth = process.env.VITE_AUTH_MODE;
|
|
1901
|
+
const hasProxy = !!cloudflareWorkerUrl;
|
|
1902
|
+
const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
|
|
1903
|
+
const config = {
|
|
1904
|
+
authMode,
|
|
1905
|
+
cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
|
|
1906
|
+
apiUrl,
|
|
1907
|
+
username,
|
|
1908
|
+
password,
|
|
1909
|
+
cfAccessClientId,
|
|
1910
|
+
cfAccessClientSecret,
|
|
1911
|
+
useCustomEndpoint: true,
|
|
1912
|
+
};
|
|
1913
|
+
try {
|
|
1914
|
+
const result = await generateGravityFormsData$1({
|
|
1915
|
+
...config,
|
|
1916
|
+
outputPath,
|
|
1917
|
+
includeInactive: false,
|
|
1918
|
+
});
|
|
1919
|
+
console.log(`📁 Gravity Forms data written to: ${path.join(process.cwd(), outputPath)}`);
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
catch (e) {
|
|
1923
|
+
console.error('❌ Gravity Forms generation failed:', e.message);
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* @license GPL-3.0-or-later
|
|
1930
|
+
*
|
|
1931
|
+
* This file is part of the MarVAlt Open SDK.
|
|
1932
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
1933
|
+
*
|
|
1934
|
+
* This program is free software: you can redistribute it and/or modify
|
|
1935
|
+
* it under the terms of the GNU General Public License as published by
|
|
1936
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
1937
|
+
* (at your option) any later version.
|
|
1938
|
+
*
|
|
1939
|
+
* This program is distributed in the hope that it will be useful,
|
|
1940
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
1941
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
1942
|
+
* See the GNU General Public License for more details.
|
|
1943
|
+
*/
|
|
1944
|
+
class MauticClient {
|
|
1945
|
+
constructor(config) {
|
|
1946
|
+
this.config = config;
|
|
1947
|
+
// Convention-based: Always use /api/mautic-submit unless explicitly overridden
|
|
1948
|
+
this.proxyEndpoint = config.proxyEndpoint || '/api/mautic-submit';
|
|
1949
|
+
}
|
|
1950
|
+
async makeRequest(endpoint, options = {}) {
|
|
1951
|
+
// Always use proxy endpoint (convention-based)
|
|
1952
|
+
const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
1953
|
+
const url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
|
|
1954
|
+
const isFormSubmission = endpoint.startsWith('/form/submit') ||
|
|
1955
|
+
(endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
|
|
1956
|
+
const headers = {
|
|
1957
|
+
...options.headers,
|
|
1958
|
+
};
|
|
1959
|
+
// Default JSON only for non-form submissions when a body exists and no explicit content-type provided
|
|
1960
|
+
const hasBody = options.body != null && options.body !== undefined;
|
|
1961
|
+
if (!isFormSubmission && hasBody && !('Content-Type' in headers)) {
|
|
1962
|
+
headers['Content-Type'] = 'application/json';
|
|
1963
|
+
}
|
|
1964
|
+
try {
|
|
1965
|
+
const response = await fetch(url, {
|
|
1966
|
+
...options,
|
|
1967
|
+
headers,
|
|
1968
|
+
});
|
|
1969
|
+
// Determine if this was a form submission based on the original endpoint
|
|
1970
|
+
const wasFormSubmission = endpoint.startsWith('/form/submit') ||
|
|
1971
|
+
(endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
|
|
1972
|
+
if (!response.ok) {
|
|
1973
|
+
// For form submissions, some Mautic setups respond with an HTML 404 after a successful submit.
|
|
1974
|
+
// Do not treat this as an error; let the frontend handle success/redirect messaging.
|
|
1975
|
+
const isRedirect = response.status >= 300 && response.status < 400;
|
|
1976
|
+
const isHtmlLike = (response.headers.get('content-type') || '').includes('text/html');
|
|
1977
|
+
if (!(wasFormSubmission && (isRedirect || response.status === 404 || isHtmlLike))) {
|
|
1978
|
+
throw new Error(`Mautic API request failed: ${response.status} ${response.statusText}`);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const responseText = await response.text();
|
|
1982
|
+
// Handle empty responses
|
|
1983
|
+
if (!responseText) {
|
|
1984
|
+
return [];
|
|
1985
|
+
}
|
|
1986
|
+
// For Mautic form submissions, the response may be HTML. Try JSON first, fallback to text.
|
|
1987
|
+
try {
|
|
1988
|
+
return JSON.parse(responseText);
|
|
1989
|
+
}
|
|
1990
|
+
catch {
|
|
1991
|
+
return responseText;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
catch (error) {
|
|
1995
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1996
|
+
console.error('Mautic API request error:', message);
|
|
1997
|
+
throw new Error(message);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Submit a form to Mautic
|
|
2002
|
+
*/
|
|
2003
|
+
async submitForm(formId, submission) {
|
|
2004
|
+
// Endpoint should be just the path, without query string (formId is in the body)
|
|
2005
|
+
const endpoint = `/form/submit`;
|
|
2006
|
+
// Encode as application/x-www-form-urlencoded to match Mautic expectations
|
|
2007
|
+
const formParams = new URLSearchParams();
|
|
2008
|
+
// Add fields
|
|
2009
|
+
Object.entries(submission.fields || {}).forEach(([alias, value]) => {
|
|
2010
|
+
if (Array.isArray(value)) {
|
|
2011
|
+
value.forEach((v) => formParams.append(`mauticform[${alias}]`, String(v)));
|
|
2012
|
+
}
|
|
2013
|
+
else if (typeof value === 'boolean') {
|
|
2014
|
+
formParams.append(`mauticform[${alias}]`, value ? '1' : '0');
|
|
2015
|
+
}
|
|
2016
|
+
else if (value != null) {
|
|
2017
|
+
formParams.append(`mauticform[${alias}]`, String(value));
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
// Required hidden fields
|
|
2021
|
+
formParams.append('mauticform[formId]', String(formId));
|
|
2022
|
+
// Prevent Mautic from attempting a redirect; frontend will handle any redirect
|
|
2023
|
+
formParams.append('mauticform[return]', '');
|
|
2024
|
+
// Mautic submit flag similar to native form button name/value
|
|
2025
|
+
formParams.append('mauticform[submit]', '1');
|
|
2026
|
+
// Include formName only if provided (derived from cachedHtml)
|
|
2027
|
+
if (submission.formName) {
|
|
2028
|
+
formParams.append('mauticform[formName]', submission.formName);
|
|
2029
|
+
}
|
|
2030
|
+
// Merge custom headers (e.g., Turnstile token) with default headers
|
|
2031
|
+
const headers = {
|
|
2032
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2033
|
+
...(submission.headers || {}),
|
|
2034
|
+
};
|
|
2035
|
+
return this.makeRequest(endpoint, {
|
|
2036
|
+
method: 'POST',
|
|
2037
|
+
headers,
|
|
2038
|
+
body: formParams.toString(),
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Create a new contact in Mautic
|
|
2043
|
+
*/
|
|
2044
|
+
async createContact(contact) {
|
|
2045
|
+
return this.makeRequest('/contacts/new', {
|
|
2046
|
+
method: 'POST',
|
|
2047
|
+
body: JSON.stringify(contact),
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Update an existing contact in Mautic
|
|
2052
|
+
*/
|
|
2053
|
+
async updateContact(contactId, contact) {
|
|
2054
|
+
return this.makeRequest(`/contacts/${contactId}/edit`, {
|
|
2055
|
+
method: 'PATCH',
|
|
2056
|
+
body: JSON.stringify(contact),
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Get a contact by email
|
|
2061
|
+
*/
|
|
2062
|
+
async getContactByEmail(email) {
|
|
2063
|
+
return this.makeRequest(`/contacts?search=email:${encodeURIComponent(email)}`);
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Get all forms from Mautic
|
|
2067
|
+
*/
|
|
2068
|
+
async getForms() {
|
|
2069
|
+
const response = await this.makeRequest('/forms');
|
|
2070
|
+
return response.forms || [];
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Get a specific form by ID
|
|
2074
|
+
*/
|
|
2075
|
+
async getForm(formId) {
|
|
2076
|
+
return this.makeRequest(`/forms/${formId}`);
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Track an event in Mautic
|
|
2080
|
+
*/
|
|
2081
|
+
async trackEvent(eventName, eventData = {}) {
|
|
2082
|
+
return this.makeRequest('/events', {
|
|
2083
|
+
method: 'POST',
|
|
2084
|
+
body: JSON.stringify({
|
|
2085
|
+
eventName,
|
|
2086
|
+
eventData,
|
|
2087
|
+
}),
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Add tags to a contact
|
|
2092
|
+
*/
|
|
2093
|
+
async addTagToContact(contactId, tags) {
|
|
2094
|
+
return this.makeRequest(`/contacts/${contactId}/tags`, {
|
|
2095
|
+
method: 'POST',
|
|
2096
|
+
body: JSON.stringify({ tags }),
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Remove tags from a contact
|
|
2101
|
+
*/
|
|
2102
|
+
async removeTagFromContact(contactId, tags) {
|
|
2103
|
+
return this.makeRequest(`/contacts/${contactId}/tags`, {
|
|
2104
|
+
method: 'DELETE',
|
|
2105
|
+
body: JSON.stringify({ tags }),
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Get contact tags
|
|
2110
|
+
*/
|
|
2111
|
+
async getContactTags(contactId) {
|
|
2112
|
+
return this.makeRequest(`/contacts/${contactId}/tags`);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Get all tags
|
|
2116
|
+
*/
|
|
2117
|
+
async getTags() {
|
|
2118
|
+
return this.makeRequest('/tags');
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Get segments
|
|
2122
|
+
*/
|
|
2123
|
+
async getSegments() {
|
|
2124
|
+
return this.makeRequest('/segments');
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Add contact to segment
|
|
2128
|
+
*/
|
|
2129
|
+
async addContactToSegment(contactId, segmentId) {
|
|
2130
|
+
return this.makeRequest(`/contacts/${contactId}/segments`, {
|
|
2131
|
+
method: 'POST',
|
|
2132
|
+
body: JSON.stringify({ segmentId }),
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Remove contact from segment
|
|
2137
|
+
*/
|
|
2138
|
+
async removeContactFromSegment(contactId, segmentId) {
|
|
2139
|
+
return this.makeRequest(`/contacts/${contactId}/segments`, {
|
|
2140
|
+
method: 'DELETE',
|
|
2141
|
+
body: JSON.stringify({ segmentId }),
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* @license GPL-3.0-or-later
|
|
2148
|
+
*
|
|
2149
|
+
* This file is part of the MarVAlt Open SDK.
|
|
2150
|
+
* Copyright (c) 2025 Vibune Pty Ltd.
|
|
2151
|
+
*
|
|
2152
|
+
* This program is free software: you can redistribute it and/or modify
|
|
2153
|
+
* it under the terms of the GNU General Public License as published by
|
|
2154
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
2155
|
+
* (at your option) any later version.
|
|
2156
|
+
*
|
|
2157
|
+
* This program is distributed in the hope that it will be useful,
|
|
2158
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
2159
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
2160
|
+
* See the GNU General Public License for more details.
|
|
2161
|
+
*/
|
|
2162
|
+
class MauticGenerator {
|
|
2163
|
+
constructor(config) {
|
|
2164
|
+
this.cachedToken = null;
|
|
2165
|
+
this.config = config;
|
|
2166
|
+
// Generator uses direct OAuth2 calls, not the client
|
|
2167
|
+
// Client is only used for proxy mode (cloudflare_proxy)
|
|
2168
|
+
this.client = new MauticClient({
|
|
2169
|
+
apiUrl: config.apiUrl,
|
|
2170
|
+
proxyEndpoint: config.cloudflareWorkerUrl,
|
|
2171
|
+
timeout: config.timeout,
|
|
2172
|
+
retries: config.retries,
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Get OAuth2 token for direct mode API calls
|
|
2177
|
+
*/
|
|
2178
|
+
async getOAuth2Token() {
|
|
2179
|
+
if (this.cachedToken && this.cachedToken.expires_at > Date.now() + 300000) {
|
|
2180
|
+
console.log('🔑 Using cached OAuth2 token');
|
|
2181
|
+
return this.cachedToken.access_token;
|
|
2182
|
+
}
|
|
2183
|
+
if (!this.config.clientId || !this.config.clientSecret) {
|
|
2184
|
+
throw new Error('OAuth2 credentials (clientId, clientSecret) required for direct mode');
|
|
2185
|
+
}
|
|
2186
|
+
console.log('🔑 Fetching new OAuth2 token...');
|
|
2187
|
+
const tokenUrl = `${this.config.apiUrl}/oauth/v2/token`;
|
|
2188
|
+
const body = new URLSearchParams({
|
|
2189
|
+
grant_type: 'client_credentials',
|
|
2190
|
+
client_id: this.config.clientId,
|
|
2191
|
+
client_secret: this.config.clientSecret,
|
|
2192
|
+
});
|
|
2193
|
+
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
|
2194
|
+
if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
|
|
2195
|
+
headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
|
|
2196
|
+
headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
|
|
2197
|
+
console.log('🔐 Added CF Access headers to OAuth2 token request');
|
|
2198
|
+
}
|
|
2199
|
+
const resp = await fetch(tokenUrl, { method: 'POST', headers, body: body.toString() });
|
|
2200
|
+
if (!resp.ok) {
|
|
2201
|
+
const errText = await resp.text();
|
|
2202
|
+
throw new Error(`OAuth2 token failed: ${resp.status} ${errText}`);
|
|
2203
|
+
}
|
|
2204
|
+
const data = await resp.json();
|
|
2205
|
+
this.cachedToken = {
|
|
2206
|
+
access_token: data.access_token,
|
|
2207
|
+
expires_at: Date.now() + (data.expires_in * 1000),
|
|
2208
|
+
};
|
|
2209
|
+
console.log('✅ OAuth2 token cached');
|
|
2210
|
+
return this.cachedToken.access_token;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Generate static data for Mautic forms
|
|
2214
|
+
*/
|
|
2215
|
+
async generateStaticData() {
|
|
2216
|
+
console.log('🔄 Generating Mautic forms JSON data...');
|
|
2217
|
+
try {
|
|
2218
|
+
const allForms = await this.fetchMauticFormsList();
|
|
2219
|
+
console.log(`📝 Found ${allForms.length} forms`);
|
|
2220
|
+
const forms = [];
|
|
2221
|
+
for (const formSummary of allForms) {
|
|
2222
|
+
const formId = formSummary.id?.toString();
|
|
2223
|
+
if (!formId) {
|
|
2224
|
+
console.warn(`⚠️ Form summary missing ID:`, formSummary);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
const formDetails = await this.fetchMauticForm(formId);
|
|
2229
|
+
if (!formDetails) {
|
|
2230
|
+
console.warn(`⚠️ No form details returned for form ${formId}`);
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
// Extract the form data with proper structure
|
|
2234
|
+
const formData = {
|
|
2235
|
+
id: formId,
|
|
2236
|
+
name: formDetails.name || formSummary.name || `Form ${formId}`,
|
|
2237
|
+
description: formDetails.description,
|
|
2238
|
+
isPublished: formDetails.isPublished || formSummary.isPublished || false,
|
|
2239
|
+
// Include post-submission behavior configuration
|
|
2240
|
+
postAction: formDetails.postAction || 'message',
|
|
2241
|
+
postActionProperty: formDetails.postActionProperty || 'Thank you for your submission!',
|
|
2242
|
+
// Include other important form properties
|
|
2243
|
+
formType: formDetails.formType || 'standalone',
|
|
2244
|
+
// Extract fields with validation and properties
|
|
2245
|
+
fields: (formDetails.fields || []).map((field) => ({
|
|
2246
|
+
id: field.id,
|
|
2247
|
+
label: field.label,
|
|
2248
|
+
alias: field.alias,
|
|
2249
|
+
type: field.type,
|
|
2250
|
+
isRequired: field.isRequired || false,
|
|
2251
|
+
validationMessage: field.validationMessage,
|
|
2252
|
+
defaultValue: field.defaultValue,
|
|
2253
|
+
properties: {
|
|
2254
|
+
placeholder: field.properties?.placeholder,
|
|
2255
|
+
cssClass: field.properties?.cssClass,
|
|
2256
|
+
validation: field.properties?.validation,
|
|
2257
|
+
options: field.properties?.options,
|
|
2258
|
+
helpText: field.properties?.helpText,
|
|
2259
|
+
size: field.properties?.size,
|
|
2260
|
+
...field.properties
|
|
2261
|
+
}
|
|
2262
|
+
})),
|
|
2263
|
+
// Extract actions
|
|
2264
|
+
actions: (formDetails.actions || []).map((action) => ({
|
|
2265
|
+
id: action.id,
|
|
2266
|
+
name: action.name,
|
|
2267
|
+
type: action.type,
|
|
2268
|
+
properties: action.properties || {}
|
|
2269
|
+
})),
|
|
2270
|
+
// Include styling and behavior
|
|
2271
|
+
cssClass: formDetails.cssClass,
|
|
2272
|
+
submitAction: formDetails.submitAction,
|
|
2273
|
+
// Keep the details for reference
|
|
2274
|
+
...formSummary
|
|
2275
|
+
};
|
|
2276
|
+
forms.push(formData);
|
|
2277
|
+
console.log(`✅ Processed form: ${formData.name} (ID: ${formId})`);
|
|
2278
|
+
}
|
|
2279
|
+
catch (error) {
|
|
2280
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2281
|
+
console.error(`❌ Error processing form ${formId}:`, message);
|
|
2282
|
+
// Continue with other forms
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
const staticData = {
|
|
2286
|
+
generated_at: new Date().toISOString(),
|
|
2287
|
+
total_forms: forms.length,
|
|
2288
|
+
forms: forms.map((form) => ({
|
|
2289
|
+
id: parseInt(form.id),
|
|
2290
|
+
name: form.name,
|
|
2291
|
+
alias: String(form.alias ?? (form.name || '').toLowerCase().replace(/\s+/g, '-')),
|
|
2292
|
+
description: form.description,
|
|
2293
|
+
isPublished: form.isPublished,
|
|
2294
|
+
fields: form.fields,
|
|
2295
|
+
actions: form.actions,
|
|
2296
|
+
cssClass: form.cssClass,
|
|
2297
|
+
submitAction: form.submitAction,
|
|
2298
|
+
postAction: form.postAction,
|
|
2299
|
+
postActionProperty: form.postActionProperty,
|
|
2300
|
+
formType: form.formType,
|
|
2301
|
+
}))
|
|
2302
|
+
};
|
|
2303
|
+
console.log(`✅ Generated static data for ${forms.length} forms`);
|
|
2304
|
+
return staticData;
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2308
|
+
console.error('❌ Error generating Mautic static data:', message);
|
|
2309
|
+
throw new Error(message);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Write static data to file
|
|
2314
|
+
*/
|
|
2315
|
+
async writeStaticData(staticData) {
|
|
2316
|
+
const outputPath = path.resolve(this.config.outputPath);
|
|
2317
|
+
const outputDir = path.dirname(outputPath);
|
|
2318
|
+
// Ensure output directory exists
|
|
2319
|
+
if (!fs.existsSync(outputDir)) {
|
|
2320
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
2321
|
+
}
|
|
2322
|
+
// Write the static data
|
|
2323
|
+
fs.writeFileSync(outputPath, JSON.stringify(staticData, null, 2));
|
|
2324
|
+
console.log(`📁 Static data written to: ${outputPath}`);
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Generate and write static data
|
|
2328
|
+
*/
|
|
2329
|
+
async generateAndWrite() {
|
|
2330
|
+
const staticData = await this.generateStaticData();
|
|
2331
|
+
await this.writeStaticData(staticData);
|
|
2332
|
+
return staticData;
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Fetch list of forms from Mautic
|
|
2336
|
+
*/
|
|
2337
|
+
async fetchMauticFormsList() {
|
|
2338
|
+
if (this.config.authMode === 'cloudflare_proxy') {
|
|
2339
|
+
const forms = await this.client.getForms();
|
|
2340
|
+
return Array.isArray(forms) ? forms : [];
|
|
2341
|
+
}
|
|
2342
|
+
// Direct mode with OAuth2
|
|
2343
|
+
const token = await this.getOAuth2Token();
|
|
2344
|
+
const url = `${this.config.apiUrl}/api/forms`;
|
|
2345
|
+
const headers = { 'Authorization': `Bearer ${token}` };
|
|
2346
|
+
if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
|
|
2347
|
+
headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
|
|
2348
|
+
headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
|
|
2349
|
+
}
|
|
2350
|
+
const resp = await fetch(url, { headers });
|
|
2351
|
+
if (!resp.ok)
|
|
2352
|
+
throw new Error(`Failed to fetch forms: ${resp.status}`);
|
|
2353
|
+
const data = await resp.json();
|
|
2354
|
+
return Array.isArray(data.forms) ? data.forms : [];
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Fetch individual form details from Mautic
|
|
2358
|
+
*/
|
|
2359
|
+
async fetchMauticForm(formId) {
|
|
2360
|
+
if (this.config.authMode === 'cloudflare_proxy') {
|
|
2361
|
+
return this.client.getForm(parseInt(formId, 10));
|
|
2362
|
+
}
|
|
2363
|
+
// Direct mode with OAuth2
|
|
2364
|
+
const token = await this.getOAuth2Token();
|
|
2365
|
+
const url = `${this.config.apiUrl}/api/forms/${formId}`;
|
|
2366
|
+
const headers = { 'Authorization': `Bearer ${token}` };
|
|
2367
|
+
if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
|
|
2368
|
+
headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
|
|
2369
|
+
headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
|
|
2370
|
+
}
|
|
2371
|
+
const resp = await fetch(url, { headers });
|
|
2372
|
+
if (!resp.ok)
|
|
2373
|
+
throw new Error(`Failed to fetch form ${formId}: ${resp.status}`);
|
|
2374
|
+
const data = await resp.json();
|
|
2375
|
+
return data.form || data;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Generate Mautic static data with configuration
|
|
2380
|
+
*/
|
|
2381
|
+
async function generateMauticData$1(config) {
|
|
2382
|
+
const generator = new MauticGenerator(config);
|
|
2383
|
+
return generator.generateAndWrite();
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
async function generateMauticData(options) {
|
|
2387
|
+
const outputPath = options?.outputPath || './src/data/mautic-data.json';
|
|
2388
|
+
const mauticUrl = process.env.MAUTIC_URL || process.env.VITE_MAUTIC_URL;
|
|
2389
|
+
const mauticProxyUrl = process.env.MAUTIC_PROXY_URL || process.env.VITE_MAUTIC_PROXY_URL;
|
|
2390
|
+
const clientId = process.env.MAUTIC_API_PUBLIC_KEY || process.env.VITE_MAUTIC_API_PUBLIC_KEY;
|
|
2391
|
+
const clientSecret = process.env.MAUTIC_API_SECRET_KEY || process.env.VITE_MAUTIC_API_SECRET_KEY;
|
|
2392
|
+
const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
|
|
2393
|
+
const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
|
|
2394
|
+
if (!mauticUrl) {
|
|
2395
|
+
console.log('⚠️ Mautic URL not configured. Skipping Mautic data generation.');
|
|
2396
|
+
return true;
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
const hasProxy = !!mauticProxyUrl;
|
|
2400
|
+
const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
|
|
2401
|
+
const result = await generateMauticData$1({
|
|
2402
|
+
authMode,
|
|
2403
|
+
apiUrl: mauticUrl,
|
|
2404
|
+
cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? mauticProxyUrl : undefined,
|
|
2405
|
+
clientId: clientId,
|
|
2406
|
+
clientSecret: clientSecret,
|
|
2407
|
+
cfAccessClientId,
|
|
2408
|
+
cfAccessClientSecret,
|
|
2409
|
+
outputPath,
|
|
2410
|
+
includeInactive: false,
|
|
2411
|
+
});
|
|
2412
|
+
console.log(`📁 Mautic data written to: ${path.join(process.cwd(), outputPath)}`);
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
catch (e) {
|
|
2416
|
+
console.error('❌ Mautic generation failed:', e.message);
|
|
2417
|
+
return false;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
async function generateSuiteCRMData(options) {
|
|
2422
|
+
const outputPath = options?.outputPath || './src/data/suitecrm-data.json';
|
|
2423
|
+
const apiUrl = process.env.VITE_SUITECRM_URL;
|
|
2424
|
+
if (!apiUrl) {
|
|
2425
|
+
console.log('⚠️ SuiteCRM URL not configured. Skipping SuiteCRM data generation.');
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
try {
|
|
2429
|
+
const hasProxy = !!process.env.VITE_SUITECRM_PROXY_URL;
|
|
2430
|
+
const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
|
|
2431
|
+
const result = await sadapter.generateSuiteCRMData({
|
|
2432
|
+
authMode,
|
|
2433
|
+
apiUrl,
|
|
2434
|
+
proxyUrl: authMode === 'cloudflare_proxy' ? process.env.VITE_SUITECRM_PROXY_URL : undefined,
|
|
2435
|
+
oauth2: {
|
|
2436
|
+
clientId: process.env.VITE_SUITECRM_CLIENT_ID,
|
|
2437
|
+
clientSecret: process.env.VITE_SUITECRM_CLIENT_SECRET,
|
|
2438
|
+
username: process.env.VITE_SUITECRM_USERNAME,
|
|
2439
|
+
password: process.env.VITE_SUITECRM_PASSWORD,
|
|
2440
|
+
},
|
|
2441
|
+
accessFunctionPath: '/api/fetch-with-access',
|
|
2442
|
+
outputPath,
|
|
2443
|
+
modules: ['Leads', 'Contacts', 'Accounts'],
|
|
2444
|
+
includeFields: true,
|
|
2445
|
+
includeRelationships: false,
|
|
2446
|
+
});
|
|
2447
|
+
console.log(`📁 SuiteCRM data written to: ${path.join(process.cwd(), outputPath)}`);
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
catch (e) {
|
|
2451
|
+
console.error('❌ SuiteCRM generation failed:', e.message);
|
|
2452
|
+
return false;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
async function generateAllData() {
|
|
2457
|
+
// Load .env files if they exist (for local development)
|
|
2458
|
+
dotenv.config();
|
|
2459
|
+
dotenv.config({ path: '.env.local' });
|
|
2460
|
+
console.log('🚀 Starting DigiVAlt static data generation...');
|
|
2461
|
+
if (process.env.VITE_IS_LOVABLE === 'true') {
|
|
2462
|
+
console.log('⚡ Lovable Environment Detected (VITE_IS_LOVABLE). Skipping static data generation to use GitHub synced static files.');
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const wpSuccess = await generateWordPressData();
|
|
2466
|
+
const gfSuccess = await generateGravityFormsData();
|
|
2467
|
+
const mauticSuccess = await generateMauticData();
|
|
2468
|
+
const suitecrmSuccess = await generateSuiteCRMData();
|
|
2469
|
+
if (!wpSuccess || !gfSuccess || !mauticSuccess || !suitecrmSuccess) {
|
|
2470
|
+
console.error('❌ Some generation tasks failed. See above for details.');
|
|
2471
|
+
process.exit(1);
|
|
2472
|
+
}
|
|
2473
|
+
console.log('✅ Generation complete!');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
exports.generateAllData = generateAllData;
|
|
2477
|
+
exports.generateGravityFormsData = generateGravityFormsData;
|
|
2478
|
+
exports.generateMauticData = generateMauticData;
|
|
2479
|
+
exports.generateSuiteCRMData = generateSuiteCRMData;
|
|
2480
|
+
exports.generateWordPressData = generateWordPressData;
|
|
2481
|
+
//# sourceMappingURL=generators.cjs.map
|